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
57 changes: 25 additions & 32 deletions src/Elastic.Documentation.Site/Assets/api-docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -2637,7 +2637,6 @@ a.docs-reference-btn::after {
--api-code-muted: var(--code-muted);
--api-code-accent: var(--color-blue-elastic-50);
--api-code-scroll: var(--code-border);
--api-code-scroll-hover: var(--color-grey-100);
overflow: visible;
position: relative;
margin-bottom: 24px;
Expand All @@ -2648,35 +2647,6 @@ a.docs-reference-btn::after {
box-shadow: none;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P2 (Medium)

This revision drops all scrollbar styling for the API code bodies, including the reset of scrollbar-width/scrollbar-color to auto and the 14px ::-webkit-scrollbar slot. The cards now just use the global * { scrollbar-width: thin; … } from styles.css. That is the setup the PR description names as the cause of the bug: on macOS the cards still get an overlay bar that only appears while scrolling, so a mouse user still has nothing to drag. The PR title and description ('A scrollbar you can grab', '14px slot that is always reserved', Firefox @supports fallback) still promise this, but the code no longer does it. Either restore the scrollbar rules or update the title and description so they don't claim this fix.

Generated by Libra. Specialist: libra-complexity. React with 👍 or 👎 to give feedback on this comment.


/* Thin scrollbar thumb on the dark surface. */
.api-code-card .api-code-sample-body,
.api-code-card .example-block-body {
scrollbar-width: thin;
scrollbar-color: var(--api-code-scroll) transparent;
}

.api-code-card .api-code-sample-body::-webkit-scrollbar,
.api-code-card .example-block-body::-webkit-scrollbar {
width: 8px;
height: 8px;
}

.api-code-card .api-code-sample-body::-webkit-scrollbar-track,
.api-code-card .example-block-body::-webkit-scrollbar-track {
background: transparent;
}

.api-code-card .api-code-sample-body::-webkit-scrollbar-thumb,
.api-code-card .example-block-body::-webkit-scrollbar-thumb {
background-color: var(--api-code-scroll);
border-radius: 9999px;
}

.api-code-card .api-code-sample-body::-webkit-scrollbar-thumb:hover,
.api-code-card .example-block-body::-webkit-scrollbar-thumb:hover {
background-color: var(--api-code-scroll-hover);
}

.example-block {
position: relative;
}
Expand Down Expand Up @@ -3978,6 +3948,27 @@ html:has(.api-page-shell) {
-webkit-user-select: none;
}

/* In a response the note sits in .example-media-body, and a sticky box cannot leave its parent: the media body
spans the full code width so the note can stay put, and everything in it that is not code (the note, a caption,
a media header with its copy button) keeps to the visible width and to the left. The panel is the container
because, as a block inside the scroll body, it is exactly the visible width; 100cqi of the scroll body itself
would include its vertical scrollbar. */
#api-examples-panel .example-response-panel {
container-type: inline-size;
}

#api-examples-panel .example-media-body {
width: max-content;
min-width: 100%;

> :not(.notranslate) {
position: sticky;
left: 0;
box-sizing: border-box;
width: 100cqi;
}
}

/* The short line carries the full explanation in a tooltip; the dotted underline says there is one. */
.api-code-carousel-card-note > span {
cursor: help;
Expand Down Expand Up @@ -4179,10 +4170,12 @@ html:has(.api-page-shell) {
overflow: auto;
}

/* 100 characters of code; the dialog fits around it (gutter and padding add themselves) and longer lines scroll sideways. */
/* At least 100 characters of code; the dialog fits around it (gutter and padding add themselves), grows with a longer
line up to the window and scrolls past that. A minimum, not a width: a fixed width would end .code-lines at 100ch,
and the sticky line numbers could not follow a longer line. */
.api-code-preview-body .highlight pre code {
box-sizing: content-box;
width: 100ch;
min-width: 100ch;
}

@media (max-width: 1023px) {
Expand Down
22 changes: 11 additions & 11 deletions src/Elastic.Documentation.Site/Assets/code-block.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,51 +205,51 @@
}
}

/* Line-number gutter (rendered by _ApiCodeBlock.cshtml): a sibling element that is excluded from
selection and copy. The wrapper owns the surface so the gutter sits on the same fill. */
/* Line-number gutter (rendered by _ApiCodeBlock.cshtml): a sibling excluded from selection and copy.
As wide as its longest line, so long lines scroll in the card while the sticky gutter stays put. */
.highlight .code-lines {
display: flex;
align-items: flex-start;
min-width: 0;
width: max-content;
min-width: 100%;
Comment thread
infra-vault-gh-plugin-prod[bot] marked this conversation as resolved.
Resolved
background-color: var(--code-bg);
border-radius: var(--code-radius);

/* width:max-content keeps long lines from wrapping; the card scrolls instead. */
> pre {
display: block;
margin: 0;
flex: 1 0 auto;
width: max-content;
min-width: 0;

code {
display: block;
width: max-content;
min-width: 0;
max-height: none;
overflow: visible;
background-color: transparent;
border-radius: 0;
padding-left: 0.5rem !important;
/* The gap after the numbers is the gutter's, so it holds while the code scrolls. */
padding-left: 0 !important;
white-space: pre;
word-break: normal;
overflow-wrap: normal;
}
}
}

/* Sticky, and opaque so code scrolls under it. */
.highlight .code-line-gutter {
position: sticky;
left: 0;
flex: 0 0 auto;
margin: 0;
border: 0;
background: transparent;
background-color: var(--code-bg);
user-select: none;
-webkit-user-select: none;
pointer-events: none;
text-align: right;
font-variant-numeric: tabular-nums;
white-space: pre;
padding: var(--code-pad) 0.5rem var(--code-pad) 1rem;
padding: var(--code-pad) 1rem;
color: var(--code-gutter);
font-family: var(--code-font);
font-size: var(--code-size);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Globalization;
using System.Text.Json;
using AwesomeAssertions;
using Elastic.Documentation.Journeys.Tests.Targets;
using Nullean.Cdp;
using Nullean.Snapshots;

namespace Elastic.Documentation.Journeys.Tests.Areas;

/// <summary>
/// Long lines in the API examples rail scroll sideways while the line numbers and the "Generated from the API spec" note
/// stay in place. Only real layout shows this, so it runs in Chrome: a journey file has no way to compare two positions.
/// The Cloud Connect spec renders in under a second, and this operation has a wide generated sample in both its request
/// and its response.
/// </summary>
[ClassDataSource<JourneyTarget>(Shared = SharedType.PerTestSession)]
[NotInParallel]
public class ApiExampleScrollingTests(JourneyTarget target)
Comment thread
infra-vault-gh-plugin-prod[bot] marked this conversation as resolved.
Resolved
{
private const string Operation = "api/doc/cloud-connect/operation/operation-patchcluster";
private const string Scenario = ".api-examples-scenario-panel:not([hidden])";

[Test]
public async Task Request_ScrollingSideways_KeepsLineNumbersAndNoteInPlace()
{
var report = await ScrollSidewaysAsync($"{Scenario} .api-code-carousel-card.is-active .api-code-carousel-card-body").ConfigureAwait(
false
);

_ = report.Should().MatchInlineSnapshot(
"""
overflows sideways: True
scrolled sideways: True
language before and after: curl, curl
line numbers from the left edge: 0px
note from the right edge: 0px
note from the bottom edge: 0px
"""
);
}

[Test]
public async Task Response_ScrollingSideways_KeepsLineNumbersAndNoteInPlace()
{
var report = await ScrollSidewaysAsync($"{Scenario} .example-block--response .example-block-body").ConfigureAwait(false);

_ = report.Should().MatchInlineSnapshot(
"""
overflows sideways: True
scrolled sideways: True
language before and after: curl, curl
line numbers from the left edge: 0px
note from the right edge: 0px
note from the bottom edge: 0px
"""
);
}

private async Task<string> ScrollSidewaysAsync(string body)
{
if (target.Name != "isolated")
Skip.Test($"The API pages these tests open are part of the isolated site, not '{target.Name}'.");

var url = new Uri(target.BaseUrl, Operation);
await WaitForPageAsync(url).ConfigureAwait(false);

await using var browser = await Browser.LaunchAsync(new BrowserLaunchOptions()).ConfigureAwait(false);
await using var context = await browser.NewContextAsync().ConfigureAwait(false);
var page = await context.NewPageAsync().ConfigureAwait(false);
await page.SetViewportAsync(1280, 900, 1).ConfigureAwait(false);
await page.GotoAsync(url.ToString()).ConfigureAwait(false);
await page.WaitForLoadStateAsync("networkidle").ConfigureAwait(false);

var before = await MeasureAsync(page, body).ConfigureAwait(false);
// A sideways wheel over the code, as a trackpad swipe or shift+wheel sends it.
await page.SendAsync("Input.dispatchMouseEvent", writer =>
{
writer.WriteString("type", "mouseWheel");
writer.WriteNumber("x", before.GetProperty("centerX").GetDouble());
writer.WriteNumber("y", before.GetProperty("centerY").GetDouble());
writer.WriteNumber("deltaX", 200);
writer.WriteNumber("deltaY", 0);
}).ConfigureAwait(false);
Comment on lines +79 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P3 (Low)

Replacing the scrollbar drag with a horizontal wheel event removes regression coverage for the interaction this PR is meant to fix. A wheel event can scroll an overflow container even when no scrollbar is visible or draggable, so these tests can pass with the scrollbar styling entirely absent. Retain a real scrollbar-drag assertion, using a browser configuration that renders scrollbars, alongside the sticky-position checks.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

await Task.Delay(300).ConfigureAwait(false);
var after = await MeasureAsync(page, body).ConfigureAwait(false);

return string.Join(
'\n',
$"overflows sideways: {before.GetProperty("overflows").GetBoolean()}",
$"scrolled sideways: {after.GetProperty("scrollLeft").GetInt32() > 0}",
$"language before and after: {before.GetProperty("language").GetString()}, {after.GetProperty("language").GetString()}",
$"line numbers from the left edge: {after.GetProperty("gutter").GetInt32()}px",
$"note from the right edge: {after.GetProperty("noteRight").GetInt32()}px",
$"note from the bottom edge: {after.GetProperty("noteBottom").GetInt32()}px"
);
}

// Edges are the scroll body's visible area: inside its border, short of its scrollbars.
private static async Task<JsonElement> MeasureAsync(Page page, string body) =>
await page.EvaluateAsync(
$$"""
(() => {
const body = document.querySelector({{JsonSerializer.Serialize(body)}});
const box = body.getBoundingClientRect();
const left = box.left + body.clientLeft, top = box.top + body.clientTop;
const gutter = body.querySelector('.code-line-gutter').getBoundingClientRect();
const note = body.querySelector('.api-code-carousel-card-note').getBoundingClientRect();
return {
language: document.querySelector('{{Scenario}} .api-code-carousel-card.is-active').dataset.lang,
overflows: body.scrollWidth > body.clientWidth,
scrollLeft: body.scrollLeft,
centerX: left + body.clientWidth / 2,
centerY: top + body.clientHeight / 2,
gutter: Math.round(gutter.left - left),
noteRight: Math.round(left + body.clientWidth - note.right),
noteBottom: Math.round(top + body.clientHeight - note.bottom),
};
})()
"""
).ConfigureAwait(false);

// The site serves an API's pages once its spec is rendered; on the isolated site that waits for every spec, Elasticsearch's too.
private static async Task WaitForPageAsync(Uri url)
{
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
var deadline = DateTime.UtcNow.AddMinutes(5);
while (DateTime.UtcNow < deadline)
{
using var response = await http.GetAsync(url).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
return;
await Task.Delay(500).ConfigureAwait(false);
}
throw new TimeoutException(string.Create(CultureInfo.InvariantCulture, $"{url} did not render within 5 minutes."));
}
}
3 changes: 2 additions & 1 deletion tests/Elastic.Documentation.Journeys.Tests/shards.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"shards": [
{ "name": "site", "classes": ["A11yJourneys", "ChromeJourneys", "ContentJourneys", "FeedbackJourneys", "LandingJourneys", "SynthJourneys", "NavigationJourneys", "PageActionsJourneys"] },
{ "name": "http-and-tool", "classes": ["DocsApiHttp", "DocsJourneysCliTests", "DocsJourneysContextTests", "DocsJourneysEmbeddedTests", "DocsJourneysExportConformanceTests", "DocsJourneysExportTests", "MarkdownAlternatesHttp", "McpHttp"] }
{ "name": "http-and-tool", "classes": ["DocsApiHttp", "DocsJourneysCliTests", "DocsJourneysContextTests", "DocsJourneysEmbeddedTests", "DocsJourneysExportConformanceTests", "DocsJourneysExportTests", "MarkdownAlternatesHttp", "McpHttp"] },
{ "name": "api-examples", "classes": ["ApiExampleScrollingTests"] }
]
}
Loading