Skip to content
Open
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
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ All changes included in 1.11:
### `html`

- ([#14684](https://github.com/quarto-dev/quarto-cli/issues/14684)): Add a "Skip to main content" link to Bootstrap-themed HTML output (documents, websites, books, dashboards) so keyboard users can bypass the navbar and sidebars.
- ([#14939](https://github.com/quarto-dev/quarto-cli/issues/14939)): Fix Plotly figures in a tab that is hidden on page load rendering at Plotly's default width instead of filling the tab when it is shown.

### `typst`

Expand Down
32 changes: 29 additions & 3 deletions src/resources/formats/html/quarto.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,42 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
}
};

// dispatch for htmlwidgets
// they use slideenter event to trigger resize
// dispatch for htmlwidgets and other resizable content
// htmlwidgets use slideenter event to trigger resize, while other libraries
// (e.g. plotly) listen for window resize
function fireSlideEnter() {
const event = window.document.createEvent("Event");
event.initEvent("slideenter", true, true);
window.document.dispatchEvent(event);
window.dispatchEvent(new Event("resize"));
}

// plotly figures drawn while their tab was hidden get plotly's default width,
// and plotly waits before handling window resize, so redraw them straight
// away to avoid showing them at the wrong size first
function resizePlotlyFigures(pane) {
if (!pane || !window.Plotly) {
return;
}
pane.querySelectorAll(".js-plotly-plot").forEach((plot) => {
// figures with a fixed size don't need resizing
const layout = plot.layout;
if (!layout || (layout.width && layout.height)) {
return;
}
window.Plotly.relayout(plot, { autosize: true });
});
}

const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');
tabs.forEach((tab) => {
tab.addEventListener("shown.bs.tab", fireSlideEnter);
tab.addEventListener("shown.bs.tab", (event) => {
const target = event.target.getAttribute("data-bs-target");
if (target && target.startsWith("#")) {
resizePlotlyFigures(window.document.getElementById(target.slice(1)));
}
fireSlideEnter();
});
});

// dispatch for shiny
Expand All @@ -96,6 +121,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
document.addEventListener(
"tabby",
function (event) {
resizePlotlyFigures(event.detail.content);
fireSlideEnter();
distpatchShinyEvents(event.detail.previousTab, event.detail.tab);
},
Expand Down
25 changes: 25 additions & 0 deletions tests/docs/playwright/html/tabsets/plotly-tabsets.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: "Plotly figures in tabsets"
format:
html:
page-layout: full
engine: jupyter
---

::: {.panel-tabset}

## Tab 1

From https://github.com/quarto-dev/quarto-cli/issues/14939

The Plotly figure in Tab 2 is drawn while its tab is hidden.

## Tab 2

```{python}
import plotly.graph_objects as go

go.Figure(go.Scatter(x=[1, 2, 3], y=[1, 3, 2]))
```

:::
26 changes: 25 additions & 1 deletion tests/integration/playwright/tests/html-tabsets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,28 @@ test('Jupyter - Creates working tabsets from for loops', async ({ page }) => {
const tab2Content = tabContent.locator('div.tab-pane').nth(1);
await expect(tab2Content).toHaveClass(/active/);
await expect(tab2Content.locator('img')).toBeVisible();
});
});

test('Plotly figures in initially hidden tabs fill the tab as soon as it is shown', async ({ page }) => {
await page.goto('/html/tabsets/plotly-tabsets.html');
const tab2 = page.getByRole('tab', { name: 'Tab 2' });
const tab2Content = page.locator('div.tab-content div.tab-pane').nth(1);
// Plotly adds a second .main-svg for hover labels; the first one holds the figure
await expect(tab2Content.locator('.js-plotly-plot .main-svg').first()).toBeAttached();
// Figures drawn while their tab is hidden fall back to Plotly's default 700px width.
// Measure in the first animation frame after the click, which runs before the
// browser paints the tab, so a figure that is only resized later fails the test.
const widths = await tab2.evaluate((tab) => new Promise<{ figure: number; container: number }>((resolve) => {
tab.click();
requestAnimationFrame(() => {
const pane = document.getElementById(tab.getAttribute('data-bs-target')!.slice(1))!;
const plot = pane.querySelector('.js-plotly-plot')!;
resolve({
figure: plot.querySelector('.main-svg')!.getBoundingClientRect().width,
container: plot.getBoundingClientRect().width,
});
});
}));
await expect(tab2Content).toHaveClass(/active/);
expect(Math.abs(widths.figure - widths.container)).toBeLessThanOrEqual(1);
});
Loading