Refactored tooltips

This commit is contained in:
squidfunk
2024-04-14 13:30:52 +07:00
parent 4eb1a43937
commit e15518cfa5
25 changed files with 779 additions and 104 deletions

View File

@@ -22,12 +22,14 @@
import {
Observable,
debounceTime,
debounce,
defer,
fromEvent,
identity,
map,
merge,
startWith
startWith,
timer
} from "rxjs"
/* ----------------------------------------------------------------------------
@@ -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"))
)
)
}

View File

@@ -30,6 +30,8 @@ import {
startWith
} from "rxjs"
import { watchElementSize } from "../../size"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
@@ -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
}
}
/* ------------------------------------------------------------------------- */
/**
@@ -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))
)
}

View File

@@ -66,6 +66,7 @@ export function watchElementContentOffset(
): Observable<ElementOffset> {
return merge(
fromEvent(el, "scroll"),
fromEvent(window, "scroll"),
fromEvent(window, "resize")
)
.pipe(

View File

@@ -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)
)
@@ -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))
)
}

View File

@@ -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
}

View File

@@ -200,7 +200,7 @@ keyboard$
})
/* Set up patches */
patchEllipsis({ document$ })
patchEllipsis({ viewport$, document$ })
patchIndeterminate({ document$, tablet$ })
patchScrollfix({ document$ })
patchScrolllock({ viewport$, tablet$ })

View File

@@ -28,8 +28,8 @@ import { Viewport, getElements } from "~/browser"
import { Component } from "../../_"
import {
Tooltip,
mountTooltip
} from "../../tooltip"
mountInlineTooltip2
} from "../../tooltip2"
import {
Annotation,
mountAnnotationBlock
@@ -131,6 +131,6 @@ export function mountContent(
/* Tooltips */
...getElements("[title]", el)
.filter(() => feature("content.tooltips"))
.map(child => mountTooltip(child))
.map(child => mountInlineTooltip2(child, { viewport$ }))
)
}

View File

@@ -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
@@ -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$ }))
}
}

View File

@@ -0,0 +1,361 @@
/*
* Copyright (c) 2016-2024 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import {
BehaviorSubject,
EMPTY,
Observable,
Subject,
animationFrameScheduler,
combineLatest,
debounce,
defer,
distinctUntilChanged,
endWith,
filter,
finalize,
first,
ignoreElements,
map,
mergeMap,
observeOn,
queueScheduler,
share,
startWith,
switchMap,
tap,
throttleTime,
timer,
withLatestFrom
} from "rxjs"
import {
ElementOffset,
Viewport,
getElement,
getElementContainers,
getElementOffsetAbsolute,
getElementSize,
watchElementContentOffset,
watchElementFocus,
watchElementHover
} from "~/browser"
import { renderInlineTooltip2 } from "~/templates"
import { Component } from "../_"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Tooltip
*/
export interface Tooltip {
active: boolean // Tooltip is active
offset: ElementOffset // Tooltip offset
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Dependencies
*/
interface Dependencies {
content$: Observable<HTMLElement> // Tooltip content observable
viewport$: Observable<Viewport> // Viewport observable
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Global sequence number for tooltips
*/
let sequence = 0
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch tooltip
*
* This function tracks the tooltip host element, and deduces the active state
* and offset of the tooltip from it. The active state is determined by whether
* the host element is focused or hovered, and the offset is determined by the
* host element's absolute position in the document.
*
* @param el - Tooltip host element
*
* @returns Tooltip observable
*/
export function watchTooltip2(
el: HTMLElement
): Observable<Tooltip> {
// Compute whether tooltip should be shown - we need to watch both focus and
// hover events on the host element and emit if one of them is active. In case
// of a hover event, we keep the element visible for a short amount of time
// after the pointer left the host element for a better user experience.
const active$ =
combineLatest([
watchElementFocus(el),
watchElementHover(el)
])
.pipe(
map(([focus, hover]) => focus || hover),
distinctUntilChanged()
)
// We need to determine all parent elements of the host element that are
// currently scrollable, as they might affect the position of the tooltip
// depending on their horizontal of vertical offset. We must track all of
// them and recompute the position of the tooltip if they change.
const offset$ =
defer(() => getElementContainers(el)).pipe(
mergeMap(watchElementContentOffset),
throttleTime(1),
map(() => getElementOffsetAbsolute(el))
)
// Only track parent elements and compute offset of the tooltip host if the
// tooltip should be shown - we defer the computation of the offset until the
// tooltip becomes active for the first time. This is necessary, because we
// must also keep the tooltip active as long as it is focused or hovered.
return active$.pipe(
first(active => active),
switchMap(() => combineLatest([active$, offset$])),
map(([active, offset]) => ({ active, offset })),
share()
)
}
/**
* Mount tooltip
*
* This function renders a tooltip with the content from the provided `content$`
* observable as passed via the dependencies. If the returned element has a role
* of type `dialog`, the tooltip is considered to be interactive, and rendered
* either above or below the host element, depending on the available space.
*
* If the returned element has a role of type `tooltip`, the tooltip is always
* rendered below the host element and considered to be non-interactive. This
* allows us to reuse the same positioning logic for both interactive and
* non-interactive tooltips, as it is largely the same.
*
* @param el - Tooltip host element
* @param dependencies - Dependencies
*
* @returns Tooltip component observable
*/
export function mountTooltip2(
el: HTMLElement, dependencies: Dependencies
): Observable<Component<Tooltip>> {
const { content$, viewport$ } = dependencies
// Compute unique tooltip id - this is necessary to associate the tooltip host
// element with the tooltip element for ARIA purposes
const id = `__tooltip2_${sequence++}`
// Create component on subscription
return defer(() => {
const push$ = new Subject<Tooltip>()
// Create subject to track tooltip presence and visibility - we use another
// purely internal subject to track the tooltip's presence and visibility,
// as the tooltip should be visible if the host element or tooltip itself
// is focused or hovered to allow for smooth pointer migration
const show$ = new BehaviorSubject(false)
push$.pipe(ignoreElements(), endWith(false))
.subscribe(show$)
// Create observable controlling tooltip element - we create and attach the
// tooltip only if it is actually present, in order to keep the number of
// elements low. We need to keep the tooltip visible for a short time after
// the pointer left the host element or tooltip itself. For this, we use an
// inner subscription to the tooltip observable, which we terminate when the
// tooltip should not be shown, automatically removing the element. Moreover
// we use the queue scheduler, which will schedule synchronously in case the
// tooltip should be shown, and asynchronously if it should be hidden.
const node$ = show$.pipe(
debounce(active => timer(+!active * 250, queueScheduler)),
distinctUntilChanged(),
switchMap(active => active ? content$ : EMPTY),
tap(node => node.id = id),
share()
)
// Compute tooltip presence and visibility - the tooltip should be shown if
// the host element or the tooltip itself is focused or hovered
combineLatest([
push$.pipe(map(({ active }) => active)),
node$.pipe(
switchMap(node => watchElementHover(node, 250)),
startWith(false)
)
])
.pipe(map(states => states.some(active => active)))
.subscribe(show$)
// Compute tooltip origin - we need to compute the tooltip origin depending
// on the position of the host element, the viewport size, as well as the
// actual size of the tooltip, if positioned above. The tooltip must about
// to be rendered for this to be correct, which is why we do it here.
const origin$ = show$.pipe(
filter(active => active),
withLatestFrom(node$, viewport$),
map(([_, node, { size }]) => {
const host = el.getBoundingClientRect()
const x = host.width / 2
// If the tooltip is non-interactive, we always render it below the
// actual element because all operating systems do it that way
if (node.role === "tooltip") {
return { x, y: 8 + host.height }
// Otherwise, we determine where there is more space, and render the
// tooltip either above or below the host element
} else if (host.y >= size.height / 2) {
const { height } = getElementSize(node)
return { x, y: -16 - height }
} else {
return { x, y: +16 + host.height }
}
})
)
// Update tooltip position - we always need to update the position of the
// tooltip, as it might change depending on the viewport offset of the host
combineLatest([node$, push$, origin$])
.subscribe(([node, { offset }, origin]) => {
node.style.setProperty("--md-tooltip-host-x", `${offset.x}px`)
node.style.setProperty("--md-tooltip-host-y", `${offset.y}px`)
// Update tooltip origin - this is mainly set to determine the position
// of the tooltip tail, to show the direction it is originating from
node.style.setProperty("--md-tooltip-x", `${origin.x}px`)
node.style.setProperty("--md-tooltip-y", `${origin.y}px`)
// Update tooltip render location, i.e., whether the tooltip is shown
// above or below the host element, depending on the available space
node.classList.toggle("md-tooltip2--top", origin.y < 0)
node.classList.toggle("md-tooltip2--bottom", origin.y >= 0)
})
// Update tooltip width - we only explicitly set the width of the tooltip
// if it is non-interactive, in case it should always be rendered centered
show$.pipe(
filter(active => active),
withLatestFrom(node$, (_, node) => node),
filter(node => node.role === "tooltip")
)
.subscribe(node => {
const size = getElementSize(getElement(":scope > *", node))
// Set tooltip width and remove tail by setting it to a width of zero -
// if authors want to keep the tail, we can move this to CSS later
node.style.setProperty("--md-tooltip-width", `${size.width}px`)
node.style.setProperty("--md-tooltip-tail", `${0}px`)
})
// Update tooltip visibility - we defer to the next animation frame, because
// the tooltip must first be added to the document before we make it appear,
// or it will appear instantly without delay. Additionally, we need to keep
// the tooltip visible for a short time after the pointer left the host.
show$.pipe(
distinctUntilChanged(),
observeOn(animationFrameScheduler),
withLatestFrom(node$)
)
.subscribe(([active, node]) => {
node.classList.toggle("md-tooltip2--active", active)
})
// Set up ARIA attributes when tooltip is visible
combineLatest([
show$.pipe(filter(active => active)),
node$
])
.subscribe(([_, node]) => {
if (node.role === "dialog") {
el.setAttribute("aria-controls", id)
el.setAttribute("aria-haspopup", "dialog")
} else {
el.setAttribute("aria-describedby", id)
}
})
// Remove ARIA attributes when tooltip is hidden
show$.pipe(filter(active => !active))
.subscribe(() => {
el.removeAttribute("aria-controls")
el.removeAttribute("aria-describedby")
el.removeAttribute("aria-haspopup")
})
// Create and return component
return watchTooltip2(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}
// ----------------------------------------------------------------------------
/**
* Mount inline tooltip
*
* @todo refactor this function
*
* @param el - Tooltip host element
* @param dependencies - Dependencies
* @param container - Container
*
* @returns Tooltip component observable
*/
export function mountInlineTooltip2(
el: HTMLElement, { viewport$ }: { viewport$: Observable<Viewport> },
container = document.body
): Observable<Component<Tooltip>> {
return mountTooltip2(el, {
content$: new Observable<HTMLElement>(observer => {
const title = el.title
const node = renderInlineTooltip2(title)
observer.next(node)
el.removeAttribute("title")
// Append tooltip and remove on unsubscription
container.append(node)
return () => {
node.remove()
el.setAttribute("title", title)
}
}),
viewport$
})
}

View File

@@ -33,10 +33,11 @@ import {
} from "rxjs"
import {
Viewport,
getElements,
watchElementVisibility
} from "~/browser"
import { mountTooltip } from "~/components"
import { mountInlineTooltip2 } from "~/components/tooltip2"
/* ----------------------------------------------------------------------------
* Helper types
@@ -47,6 +48,7 @@ import { mountTooltip } from "~/components"
*/
interface PatchOptions {
document$: Observable<Document> /* Document observable */
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
@@ -64,7 +66,7 @@ interface PatchOptions {
* @param options - Options
*/
export function patchEllipsis(
{ document$ }: PatchOptions
{ document$, viewport$ }: PatchOptions
): void {
document$
.pipe(
@@ -84,7 +86,7 @@ export function patchEllipsis(
host.title = text
/* Mount tooltip */
return mountTooltip(host)
return mountInlineTooltip2(host, { viewport$ })
.pipe(
takeUntil(document$.pipe(skip(1))),
finalize(() => host.removeAttribute("title"))
@@ -97,7 +99,7 @@ export function patchEllipsis(
document$
.pipe(
switchMap(() => getElements(".md-status")),
mergeMap(el => mountTooltip(el))
mergeMap(el => mountInlineTooltip2(el, { viewport$ }))
)
.subscribe()
}

View File

@@ -20,6 +20,8 @@
* IN THE SOFTWARE.
*/
import { ComponentChild } from "preact"
import { h } from "~/utilities"
/* ----------------------------------------------------------------------------
@@ -61,3 +63,16 @@ export function renderTooltip(
)
}
}
// @todo: rename
export function renderInlineTooltip2(
...children: ComponentChild[]
): HTMLElement {
return (
<div class="md-tooltip2" role="tooltip">
<div class="md-tooltip2__inner md-typeset">
{children}
</div>
</div>
)
}

View File

@@ -66,6 +66,7 @@
@import "main/components/tabs";
@import "main/components/tag";
@import "main/components/tooltip";
@import "main/components/tooltip2";
@import "main/components/top";
@import "main/components/version";

View File

@@ -0,0 +1,203 @@
////
/// Copyright (c) 2016-2024 Martin Donath <martin.donath@squidfunk.com>
///
/// Permission is hereby granted, free of charge, to any person obtaining a
/// copy of this software and associated documentation files (the "Software"),
/// to deal in the Software without restriction, including without limitation
/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
/// and/or sell copies of the Software, and to permit persons to whom the
/// Software is furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
/// DEALINGS
////
// ----------------------------------------------------------------------------
// Rules
// ----------------------------------------------------------------------------
// Tooltip variables
:root {
--md-tooltip-width: #{px2rem(400px)};
--md-tooltip-tail: #{px2rem(6px)};
}
// ----------------------------------------------------------------------------
// Tooltip
.md-tooltip2 {
position: absolute;
// Note that the top offset is computed from the host element offset plus the
// tooltip offset, which is always measured relative to the host element
top:
calc(
var(--md-tooltip-host-y) +
var(--md-tooltip-y)
);
inline-size: 100%;
// Hack: set an explicit `z-index` so we can transition it to ensure that any
// following elements are not overlaying the tooltip during the transition.
z-index: 0;
font-family: var(--md-text-font-family);
color: var(--md-default-fg-color);
pointer-events: none;
opacity: 0;
transition:
transform 0ms 250ms,
opacity 250ms,
z-index 250ms;
transform: translateY(px2rem(-8px));
// We explicitly set the origin to the tooltip tail, allowing the author to
// easily add further transforms to the tooltip, customizing the transition
transform-origin:
calc(
var(--md-tooltip-host-x) +
var(--md-tooltip-x)
)
0;
// Hack: promote to own layer to reduce jitter
backface-visibility: hidden;
// Tooltip tail
&::before {
position: absolute;
// The offset of the tooltip tail is computed from the host element offset,
// plus the tooltip offset, which equals the center of the host element,
// and minus the half width of the tooltip tail to center it. Then, on both
// sides, the tooltip tail is padded with 150% of the inset area.
left:
clamp(
1.5 * #{px2rem(16px)},
calc(
var(--md-tooltip-host-x) +
var(--md-tooltip-x) -
var(--md-tooltip-tail)
),
calc(
100vw -
2 * var(--md-tooltip-tail) -
1.5 * #{px2rem(16px)}
)
);
z-index: 1;
display: block;
content: "";
border-inline: var(--md-tooltip-tail) solid transparent;
}
// Tooltip tail if rendered above target
&--top::before {
bottom: calc(-1 * var(--md-tooltip-tail) + px2rem(0.5px));
filter: drop-shadow(0 1px 0 hsla(0, 0%, 0%, 0.05));
border-top: var(--md-tooltip-tail) solid var(--md-default-bg-color);
}
// Tooltip tail if rendered below target
&--bottom::before {
top: calc(-1 * var(--md-tooltip-tail) + px2rem(0.5px));
filter: drop-shadow(0 -1px 0 hsla(0, 0%, 0%, 0.05));
border-bottom: var(--md-tooltip-tail) solid var(--md-default-bg-color);
}
// Tooltip is visible
&--active {
z-index: 2;
opacity: 1;
transition:
transform 400ms cubic-bezier(0, 1, 0.5, 1),
opacity 250ms,
z-index 0ms;
transform: translateY(0);
}
// Tooltip wrapper
&__inner {
position: relative;
// The tooltip is slightly moved to the left, so it nicely aligns with the
// content of the tooltip set by the padding of this element. On both sides,
// the tooltip is padded with the inset area, so it never touches the edge
// of the window for a better user experience.
left:
clamp(
#{px2rem(16px)},
calc(
var(--md-tooltip-host-x) -
#{px2rem(16px)}
),
calc(
100vw -
var(--md-tooltip-width) -
#{px2rem(16px)}
)
);
max-width: calc(100vw - 2 * #{px2rem(16px)});
max-height: 40vh;
background-color: var(--md-default-bg-color);
border-radius: px2rem(2px);
box-shadow: var(--md-shadow-z2);
scrollbar-width: thin;
scrollbar-gutter: stable;
// Webkit scrollbar
&::-webkit-scrollbar {
width: px2rem(4px);
height: px2rem(4px);
}
// Webkit scrollbar thumb
&::-webkit-scrollbar-thumb {
background-color: var(--md-default-fg-color--lighter);
// Webkit scrollbar thumb on hover
&:hover {
background-color: var(--md-accent-fg-color);
}
}
// Tooltip is non-interactive - this role should be set if the tooltip has
// only informational and non-interactive content, e.g., an actual tooltip.
// It has no explicitl width set, uses a smaller font, and is centered,
// other than a tooltip with typesetted content.
[role="tooltip"] > & {
left:
clamp(
#{px2rem(16px)},
calc(
var(--md-tooltip-host-x) +
var(--md-tooltip-x) -
var(--md-tooltip-width) / 2
),
calc(
100vw -
var(--md-tooltip-width) -
#{px2rem(16px)}
)
);
width: fit-content;
padding: px2rem(4px) px2rem(8px);
font-size: px2rem(10px);
font-weight: 700;
// If the author wishes to keep the tooltip visible upon hover and make
// the text selectable, this property can be set to `initial`
user-select: none;
}
// Adjust spacing on first child
&.md-typeset > :first-child {
margin-top: 0;
}
// Adjust spacing on last child
&.md-typeset > :last-child {
margin-bottom: 0;
}
}
}