Skip to content

Commit

Permalink
Merge pull request #7045 from squidfunk/refactor/tooltip-positioning
Browse files Browse the repository at this point in the history
Refactor tooltips
  • Loading branch information
squidfunk authored Apr 16, 2024
2 parents 851e5bb + 29658ed commit f028004
Show file tree
Hide file tree
Showing 25 changed files with 786 additions and 104 deletions.
18 changes: 0 additions & 18 deletions material/overrides/assets/javascripts/custom.129bd6ad.min.js

This file was deleted.

18 changes: 18 additions & 0 deletions material/overrides/assets/javascripts/custom.e2e97759.min.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion material/overrides/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ 'assets/javascripts/custom.129bd6ad.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/custom.e2e97759.min.js' | url }}"></script>
{% endblock %}
29 changes: 29 additions & 0 deletions material/templates/assets/javascripts/bundle.3220b9d7.min.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

29 changes: 0 additions & 29 deletions material/templates/assets/javascripts/bundle.ae821067.min.js

This file was deleted.

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

This file was deleted.

4 changes: 2 additions & 2 deletions material/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
{% endif %}
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.8b0efcb2.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.66ac8b77.min.css' | url }}">
{% if config.theme.palette %}
{% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.06af60db.min.css' | url }}">
Expand Down Expand Up @@ -249,7 +249,7 @@
</script>
{% endblock %}
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.ae821067.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.3220b9d7.min.js' | url }}"></script>
{% for script in config.extra_javascript %}
{{ script | script_tag }}
{% endfor %}
Expand Down
22 changes: 15 additions & 7 deletions src/templates/assets/javascripts/browser/element/hover/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@

import {
Observable,
debounceTime,
debounce,
defer,
fromEvent,
identity,
map,
merge,
startWith
startWith,
timer
} from "rxjs"

/* ----------------------------------------------------------------------------
Expand All @@ -37,20 +39,26 @@ import {
/**
* Watch element hover
*
* The second parameter allows to specify a timeout in milliseconds after which
* the hover state will be reset to `false`. This is useful for tooltips which
* should disappear after a certain amount of time, in order to allow the user
* to move the cursor from the host to the tooltip.
*
* @param el - Element
* @param duration - Debounce duration
* @param timeout - Timeout
*
* @returns Element hover observable
*/
export function watchElementHover(
el: HTMLElement, duration?: number
el: HTMLElement, timeout?: number
): Observable<boolean> {
return merge(
return defer(() => merge(
fromEvent(el, "mouseenter").pipe(map(() => true)),
fromEvent(el, "mouseleave").pipe(map(() => false))
)
.pipe(
duration ? debounceTime(duration) : identity,
startWith(false)
timeout ? debounce(active => timer(+!active * timeout)) : identity,
startWith(el.matches(":hover"))
)
)
}
39 changes: 39 additions & 0 deletions src/templates/assets/javascripts/browser/element/offset/_/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
startWith
} from "rxjs"

import { watchElementSize } from "../../size"

/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
Expand Down Expand Up @@ -62,6 +64,23 @@ export function getElementOffset(
}
}

/**
* Retrieve absolute element offset
*
* @param el - Element
*
* @returns Element offset
*/
export function getElementOffsetAbsolute(
el: HTMLElement
): ElementOffset {
const rect = el.getBoundingClientRect()
return {
x: rect.x + window.scrollX,
y: rect.y + window.scrollY
}
}

/* ------------------------------------------------------------------------- */

/**
Expand All @@ -84,3 +103,23 @@ export function watchElementOffset(
startWith(getElementOffset(el))
)
}

/**
* Watch absolute element offset
*
* @param el - Element
*
* @returns Element offset observable
*/
export function watchElementOffsetAbsolute(
el: HTMLElement
): Observable<ElementOffset> {
return merge(
watchElementOffset(el),
watchElementSize(document.body) // @todo find a better way for this
)
.pipe(
map(() => getElementOffsetAbsolute(el)),
startWith(getElementOffsetAbsolute(el))
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function watchElementContentOffset(
): Observable<ElementOffset> {
return merge(
fromEvent(el, "scroll"),
fromEvent(window, "scroll"),
fromEvent(window, "resize")
)
.pipe(
Expand Down
50 changes: 29 additions & 21 deletions src/templates/assets/javascripts/browser/element/size/_/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,12 @@ const observer$ = defer(() => (
: of(undefined)
))
.pipe(
map(() => new ResizeObserver(entries => {
for (const entry of entries)
entry$.next(entry)
})),
switchMap(observer => merge(NEVER, of(observer))
.pipe(
finalize(() => observer.disconnect())
)
),
map(() => new ResizeObserver(entries => (
entries.forEach(entry => entry$.next(entry))
))),
switchMap(observer => merge(NEVER, of(observer)).pipe(
finalize(() => observer.disconnect())
)),
shareReplay(1)
)

Expand Down Expand Up @@ -136,16 +133,27 @@ export function getElementSize(
export function watchElementSize(
el: HTMLElement
): Observable<ElementSize> {
return observer$
.pipe(
tap(observer => observer.observe(el)),
switchMap(observer => entry$
.pipe(
filter(({ target }) => target === el),
finalize(() => observer.unobserve(el)),
map(() => getElementSize(el))
)
),
startWith(getElementSize(el))
)

// Compute target element - since inline elements cannot be observed by the
// current `ResizeObserver` implementation as provided by browsers, we need
// to determine the first containing parent element and use that one as a
// target, while we always compute the actual size from the element.
let target = el
while (target.clientWidth === 0)
if (target.parentElement)
target = target.parentElement
else
break

// Observe target element and recompute element size on resize - as described
// above, the target element is not necessarily the element of interest
return observer$.pipe(
tap(observer => observer.observe(target)),
switchMap(observer => entry$.pipe(
filter(entry => entry.target === target),
finalize(() => observer.unobserve(target))
)),
map(() => getElementSize(el)),
startWith(getElementSize(el))
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,40 @@ export function getElementContainer(
/* Return overflowing container */
return parent ? el : undefined
}

/**
* Retrieve all overflowing containers of an element, if any
*
* Note that this function has a slightly different behavior, so we should at
* some point consider refactoring how overflowing containers are handled.
*
* @param el - Element
*
* @returns Overflowing containers
*/
export function getElementContainers(
el: HTMLElement
): HTMLElement[] {
const containers: HTMLElement[] = []

// Walk up the DOM tree until we find an overflowing container
let parent = el.parentElement
while (parent) {
if (
el.clientWidth > parent.clientWidth ||
el.clientHeight > parent.clientHeight
)
containers.push(parent)

// Continue with parent element
parent = (el = parent).parentElement
}

// If the page is short, the body might not be overflowing and there might be
// no other containers, which is why we need to make sure the body is present
if (containers.length === 0)
containers.push(document.documentElement)

// Return overflowing containers
return containers
}
2 changes: 1 addition & 1 deletion src/templates/assets/javascripts/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ keyboard$
})

/* Set up patches */
patchEllipsis({ document$ })
patchEllipsis({ viewport$, document$ })
patchIndeterminate({ document$, tablet$ })
patchScrollfix({ document$ })
patchScrolllock({ viewport$, tablet$ })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import { Viewport, getElements } from "~/browser"
import { Component } from "../../_"
import {
Tooltip,
mountTooltip
} from "../../tooltip"
mountInlineTooltip2
} from "../../tooltip2"
import {
Annotation,
mountAnnotationBlock
Expand Down Expand Up @@ -131,6 +131,6 @@ export function mountContent(
/* Tooltips */
...getElements("[title]", el)
.filter(() => feature("content.tooltips"))
.map(child => mountTooltip(child))
.map(child => mountInlineTooltip2(child, { viewport$ }))
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ import {
watchElementSize,
watchElementVisibility
} from "~/browser"
import {
Tooltip,
mountInlineTooltip2
} from "~/components/tooltip2"
import { renderClipboardButton } from "~/templates"

import { Component } from "../../../_"
import {
Tooltip,
mountTooltip
} from "../../../tooltip"
import {
Annotation,
mountAnnotationList
Expand Down Expand Up @@ -200,7 +200,7 @@ export function mountCodeBlock(
const button = renderClipboardButton(parent.id)
parent.insertBefore(button, el)
if (feature("content.tooltips"))
content$.push(mountTooltip(button))
content$.push(mountInlineTooltip2(button, { viewport$ }))
}
}

Expand Down
Loading

0 comments on commit f028004

Please sign in to comment.