Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import { setExternalLinkAttributes } from './modules/external-links.js';
import { monitorInputType } from './modules/input-type.js';
import { enableSharing } from './modules/share.js';
import { highlightCurrentHeading } from './modules/toc.js';
import { enhanceTooltips } from './modules/tooltips';

const resizedEventName = addResizedEvent();

enhanceTooltips();
setClickableBlocks();
setExternalLinkAttributes();
addStickyNavigation(
Expand Down
36 changes: 36 additions & 0 deletions src/scripts/modules/tooltips.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const SELECTOR = '[data-tooltip]';

function addTooltipSpan(host: Element): void {
let bubble = host.querySelector(':scope > .tooltip');
if (!bubble) {
bubble = document.createElement('span');
bubble.className = 'tooltip';
bubble.setAttribute('aria-hidden', 'true');
host.append(bubble);
}
bubble.textContent = (host as HTMLElement).dataset.tooltip ?? '';
}

export function enhanceTooltips(): void {
// Every tooltip that exists now needs an associated span
document.querySelectorAll(SELECTOR).forEach(addTooltipSpan);

// As do any that are dynamically added later
new MutationObserver((records) => {
for (const record of records) {
if (record.type === 'attributes' && record.target instanceof Element) {
addTooltipSpan(record.target);
}
for (const node of record.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.matches(SELECTOR)) addTooltipSpan(node);
node.querySelectorAll(SELECTOR).forEach(addTooltipSpan);
}
}
}).observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-tooltip'],
});
}
54 changes: 25 additions & 29 deletions src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2867,12 +2867,18 @@ a[data-youtube] {

/* Edge-aligned: the button sits at the block's right edge, where a centred
bubble would hang off the side of the page. */
.code-block .code-block__copy::after {
.code-block .code-block__copy .tooltip {
left: auto;
right: 0;
translate: 0 var(--tooltip-shift);
}

.code-block .code-block__copy .tooltip::after {
left: auto;
right: calc(1rem - var(--tooltip-arrow-size));
translate: 0 calc(-1 * var(--tooltip-arrow-overlap));
}

/* The single fixed language */
.code-block__language {
font: var(--textBodyRegularMedium);
Expand Down Expand Up @@ -3088,15 +3094,10 @@ a.button.button--primary:active {
--tooltip-arrow-size: 7px;
--tooltip-offset: calc(var(--space8) + var(--borderWidth1));
--tooltip-arrow-overlap: 1px;
--tooltip-arrow-offset: calc(
var(--tooltip-offset) - var(--tooltip-arrow-size) +
var(--tooltip-arrow-overlap)
);
}

/* The bubble */
[data-tooltip]::after {
content: attr(data-tooltip);
.tooltip {
position: absolute;
left: 50%;
inset-block-end: calc(100% + var(--tooltip-offset));
Expand All @@ -3110,56 +3111,51 @@ a.button.button--primary:active {
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition:
opacity 375ms cubic-bezier(0.165, 0.84, 0.44, 1),
translate 375ms cubic-bezier(0.165, 0.84, 0.44, 1);

@media (prefers-reduced-motion: reduce) {
transition: none;
}
}

/* The arrow. A border triangle */
[data-tooltip]::before {
.tooltip::after {
content: '';
position: absolute;
left: 50%;
inset-block-end: calc(100% + var(--tooltip-arrow-offset));
translate: -50% var(--tooltip-shift);
top: 100%;
translate: -50% calc(-1 * var(--tooltip-arrow-overlap));
width: 0;
height: 0;
border-inline: var(--tooltip-arrow-size) solid transparent;
border-block-start: var(--tooltip-arrow-size) solid
var(--colorBackgroundInversePrimary);
border-inline: var(--tooltip-arrow-size) solid transparent;
pointer-events: none;
opacity: 0;
}

[data-tooltip][data-tooltip-position='bottom'] {
--tooltip-shift: -10px;
}

[data-tooltip][data-tooltip-position='bottom']::after {
[data-tooltip][data-tooltip-position='bottom'] .tooltip {
inset-block: calc(100% + var(--tooltip-offset)) auto;
}

[data-tooltip][data-tooltip-position='bottom']::before {
inset-block: calc(100% + var(--tooltip-arrow-offset)) auto;
[data-tooltip][data-tooltip-position='bottom'] .tooltip::after {
top: auto;
bottom: 100%;
translate: -50% var(--tooltip-arrow-overlap);
border-block-start: 0;
border-block-end: var(--tooltip-arrow-size) solid
var(--colorBackgroundInversePrimary);
}

[data-tooltip]::before,
[data-tooltip]::after {
transition:
opacity 375ms cubic-bezier(0.165, 0.84, 0.44, 1),
translate 375ms cubic-bezier(0.165, 0.84, 0.44, 1);

@media (prefers-reduced-motion: reduce) {
transition: none;
}
}

[data-tooltip]:is(:hover, :focus-visible, [data-copied], [data-failed]) {
--tooltip-shift: 0px;
}

[data-tooltip]:is(:hover, :focus-visible, [data-copied], [data-failed])::before,
[data-tooltip]:is(:hover, :focus-visible, [data-copied], [data-failed])::after {
[data-tooltip]:is(:hover, :focus-visible, [data-copied], [data-failed]) .tooltip {
opacity: 1;
transition-duration: 350ms;
}
Expand Down
31 changes: 16 additions & 15 deletions tests/copy-button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,24 @@ test('the tooltip arrow overlaps the bubble it points from', async ({
}) => {
await page.goto(PAGE);

const geometry = await page
.locator('.copy-heading-url')
.first()
.evaluate((button) => {
const bubble = getComputedStyle(button, '::after');
const arrow = getComputedStyle(button, '::before');
return {
bubbleBottom: parseFloat(bubble.bottom),
arrowBottom: parseFloat(arrow.bottom),
arrowHeight: parseFloat(arrow.borderTopWidth),
};
});
const button = page.locator('.copy-heading-url').first();
await button.locator('.tooltip').waitFor({ state: 'attached' });

const overlap = await button.evaluate((el) => {
const bubble = el.querySelector('.tooltip');
if (!bubble) throw new Error('expected the button to have a .tooltip bubble');

const caret = getComputedStyle(bubble, '::after');
const bubbleHeight = bubble.getBoundingClientRect().height;
const caretTop = parseFloat(caret.top);
const caretShiftY = parseFloat(caret.translate.split(' ')[1] ?? '0');

return bubbleHeight - (caretTop + caretShiftY);
});

const arrowTop = geometry.arrowBottom + geometry.arrowHeight;
expect(
arrowTop - geometry.bubbleBottom,
'expected the arrow to reach past the bubble’s edge'
overlap,
'expected the caret to reach past the bubble’s edge'
).toBeGreaterThan(0);
});

Expand Down