Skip to content

Commit a7b9e55

Browse files
github-actions[bot]Copilotdsymenojaf
authored
[Repo Assist] Use Popover API for code snippet tooltips; fix scroll-offset positioning (#1061)
* Use Popover API for tooltip divs; fix scroll-offset positioning bug - Add `popover` attribute to `div.fsdocs-tip` elements in HtmlFormatting.fs, placing them in the browser top layer when supported (Baseline 2024). - Update fsdocs-tips.js to call showPopover()/hidePopover() on browsers that support the Popover API; fall back to display:block/none on older browsers. - Switch to `position: fixed` in the Popover API path (correct for top-layer elements) and fix a scroll-offset bug where tooltips appeared at wrong positions when the page was scrolled. - Fix a minor bug in the right-edge overflow correction (was using `y` instead of `x` as the base for the left-shift calculation). - Add `div.fsdocs-tip:popover-open { display: block }` to fsdocs-default.css so author-level display:none does not suppress the Popover API reveal. - Update RELEASE_NOTES.md. Closes #422 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger CI checks * fix: resolve popover black-background regression; drop fallback; add fade-in - CSS: add `inset: unset` to `div.fsdocs-tip:popover-open` to reset the UA popover stylesheet's `inset: 0` (i.e. right:0 / bottom:0), which was stretching the element across the viewport and causing the visible dark area around the tooltip. - CSS: add `position: fixed` explicitly in the :popover-open rule so the element is correctly anchored to the JS-supplied left/top coordinates. - CSS: add a 120ms ease-out opacity fade-in animation for a subtle reveal. - JS: remove the display-toggle fallback branch entirely. The Popover API is Baseline 2024 (Chrome 114+, Firefox 125+, Safari 17+) and the technical audience of fsdocs users will have modern browsers. - RELEASE_NOTES: update entry to reflect the simplified (no-fallback) design and the animation addition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks * fix: replace onmouseout/onmouseover with data-* attrs + event delegation; fix Chrome popover background - HtmlFormatting.fs: emit data-fsdocs-tip / data-fsdocs-tip-unique instead of inline onmouseover/onmouseout handlers; overlapping vs non-overlapping cases now produce identical output (owner param is no longer needed) - GenerateHtml.fs (ApiDocs): same data-* attrs on code elements; add the popover attribute to fsdocs-tip divs so showPopover() works there too - fsdocs-tips.js: add delegated mouseover/mouseout listeners that read data-fsdocs-tip attributes; wrap showPopover() in try/catch for safety; keep showTip/hideTip on window for backward-compat with cached docs - fsdocs-default.css: add explicit background-color inside :popover-open to override Chrome UA top-layer canvas default (fixes black background); add [data-fsdocs-tip] cursor rule alongside legacy span[onmouseout] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks * Fix backdrop * Tweak css * Code cleanup * fix: update test assertion to match new data-fsdocs-tip attribute convention The ConvertCommand test checked that 'fsdocs-tip' is not in the output when no template is given. Since the span elements now use data-fsdocs-tip attributes (instead of onmouseover/onmouseout inline handlers), the output always contains 'fsdocs-tip' as part of the attribute name. The test intent is to verify that tooltip *div* elements (class="fsdocs-tip") are not emitted without a template. Update the assertion to check for class="fsdocs-tip" specifically, which only matches the tooltip divs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com> Co-authored-by: nojaf <florian.verdonck@outlook.com>
1 parent 8b7b708 commit a7b9e55

6 files changed

Lines changed: 78 additions & 67 deletions

File tree

RELEASE_NOTES.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@
22

33
## [Unreleased]
44

5-
### Refactored
6-
* Split `MarkdownParser.fs` (1500 lines) into `MarkdownInlineParser.fs` (inline formatting) and `MarkdownParser.fs` (block-level parsing) for better maintainability. [#1022](https://github.com/fsprojects/FSharp.Formatting/issues/1022)
7-
* Split pipe-table and Emacs-table parsing out of `MarkdownBlockParser.fs` into a new `MarkdownTableParser.fs` (196 lines), reducing `MarkdownBlockParser.fs` from 958 to 760 lines. [#1022](https://github.com/fsprojects/FSharp.Formatting/issues/1022)
8-
95
### Added
106
* Add `dotnet fsdocs convert` command to convert a single `.md`, `.fsx`, or `.ipynb` file to HTML (or another output format) without building a full documentation site. [#811](https://github.com/fsprojects/FSharp.Formatting/issues/811)
117
* `fsdocs convert` now accepts the input file as a positional argument (e.g. `fsdocs convert notebook.ipynb -o notebook.html`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
128
* `fsdocs convert` infers the output format from the output file extension when `--outputformat` is not specified (e.g. `-o out.md` implies `--outputformat markdown`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
139
* `fsdocs convert` now accepts `-o` as a shorthand for `--output`. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
1410

1511
### Changed
12+
* Tooltip elements (`div.fsdocs-tip`) now use the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) (Baseline 2024: Chrome 114+, Firefox 125+, Safari 17+). Tooltips are placed in the browser's top layer — no `z-index` needed, always above all other content. Fixes a positioning bug where tooltips appeared offset when the page was scrolled. The previous `display`-toggle fallback has been removed. Tooltips also fade in with a subtle animation. [#422](https://github.com/fsprojects/FSharp.Formatting/issues/422), [#1061](https://github.com/fsprojects/FSharp.Formatting/pull/1061)
13+
* Generated code tokens no longer use inline `onmouseover`/`onmouseout` event handlers. Tooltips are now triggered via `data-fsdocs-tip` / `data-fsdocs-tip-unique` attributes and a delegated event listener in `fsdocs-tips.js`. The `popover` attribute is also added to API-doc tooltip divs so they use the same top-layer path. [#1061](https://github.com/fsprojects/FSharp.Formatting/pull/1061)
1614
* Changed `range` fields in `MarkdownSpan` and `MarkdownParagraph` DU cases from `MarkdownRange option` to `MarkdownRange`, using `MarkdownRange.zero` as the default/placeholder value instead of `None`.
1715
* When no template is provided (e.g. `fsdocs convert` without `--template`), `fsdocs-tip` tooltip divs are no longer included in the output. Tooltips require JavaScript/CSS from a template to function, so omitting them produces cleaner raw output. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
1816

docs/content/fsdocs-default.css

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -983,8 +983,6 @@ table.pre, code, pre.fssnip {
983983

984984
/* tooltips */
985985
div.fsdocs-tip {
986-
z-index: 1000;
987-
display: none;
988986
background-color: var(--doc-tip-background);
989987
border-radius: var(--radius);
990988
border: 1px solid var(--header-border);
@@ -993,16 +991,30 @@ div.fsdocs-tip {
993991
font-variant-ligatures: none;
994992
color: var(--code-color);
995993
box-shadow: 0 1px 1px var(--shadow-color);
994+
margin: 0;
996995

997996
& code {
998997
color: var(--code-color);
999998
}
1000999
}
10011000

1002-
span[onmouseout] {
1001+
@keyframes fsdocs-tip-fade-in {
1002+
from { opacity: 0; }
1003+
to { opacity: 1; }
1004+
}
1005+
1006+
/* Reset UA inset so JS-supplied left/top control position. */
1007+
div.fsdocs-tip:popover-open {
1008+
position: fixed;
1009+
inset: unset;
1010+
animation: fsdocs-tip-fade-in 120ms ease-out;
1011+
}
1012+
1013+
[data-fsdocs-tip] {
10031014
cursor: pointer;
10041015
}
10051016

1017+
10061018
/* API docs */
10071019
#content > div > h2:first-child {
10081020
margin-top: 0;
@@ -1212,7 +1224,7 @@ span[onmouseout] {
12121224
}
12131225

12141226
/* Search */
1215-
::backdrop {
1227+
dialog::backdrop {
12161228
background-color: #020202;
12171229
opacity: 0.5;
12181230
}

docs/content/fsdocs-tips.js

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
let currentTip = null;
22
let currentTipElement = null;
33

4-
function hideTip(evt, name, unique) {
4+
function hideTip(name) {
55
const el = document.getElementById(name);
6-
el.style.display = "none";
6+
if (el) {
7+
try { el.hidePopover(); } catch (_) { }
8+
}
79
currentTip = null;
10+
currentTipElement = null;
811
}
912

10-
function hideUsingEsc(e) {
11-
hideTip(e, currentTipElement, currentTip);
12-
}
13-
14-
function showTip(evt, name, unique, owner) {
15-
document.onkeydown = hideUsingEsc;
13+
function showTip(evt, name, unique) {
1614
if (currentTip === unique) return;
15+
16+
// Hide the previously shown tooltip before showing the new one
17+
if (currentTipElement !== null) {
18+
const prev = document.getElementById(currentTipElement);
19+
if (prev) {
20+
try { prev.hidePopover(); } catch (_) { }
21+
}
22+
}
23+
1724
currentTip = unique;
1825
currentTipElement = name;
1926

@@ -22,28 +29,45 @@ function showTip(evt, name, unique, owner) {
2229
let y = evt.clientY + offset;
2330

2431
const el = document.getElementById(name);
25-
el.style.position = "absolute";
26-
el.style.display = "block";
27-
el.style.left = `${x}px`;
28-
el.style.top = `${y}px`;
2932
const maxWidth = document.documentElement.clientWidth - x - 16;
3033
el.style.maxWidth = `${maxWidth}px`;
34+
el.style.left = `${x}px`;
35+
el.style.top = `${y}px`;
36+
37+
try { el.showPopover(); } catch (_) { }
3138

32-
const rect = el.getBoundingClientRect();
33-
// Move tooltip if it is out of sight
34-
if(rect.bottom > window.innerHeight) {
39+
const rect = el.getBoundingClientRect();
40+
// Move tooltip if it would appear outside the viewport
41+
if (rect.bottom > window.innerHeight) {
3542
y = y - el.clientHeight - offset;
3643
el.style.top = `${y}px`;
3744
}
38-
3945
if (rect.right > window.innerWidth) {
40-
x = y - el.clientWidth - offset;
46+
x = x - el.clientWidth - offset;
4147
el.style.left = `${x}px`;
42-
const maxWidth = document.documentElement.clientWidth - x - 16;
43-
el.style.maxWidth = `${maxWidth}px`;
48+
el.style.maxWidth = `${document.documentElement.clientWidth - x - 16}px`;
4449
}
4550
}
4651

52+
// Event delegation: trigger tooltips from data-fsdocs-tip attributes
53+
document.addEventListener('mouseover', function (evt) {
54+
const target = evt.target.closest('[data-fsdocs-tip]');
55+
if (!target) return;
56+
const name = target.dataset.fsdocsTip;
57+
const unique = parseInt(target.dataset.fsdocsTipUnique, 10);
58+
showTip(evt, name, unique);
59+
});
60+
61+
document.addEventListener('mouseout', function (evt) {
62+
const target = evt.target.closest('[data-fsdocs-tip]');
63+
if (!target) return;
64+
// Only hide when the mouse has left the trigger element entirely
65+
if (target.contains(evt.relatedTarget)) return;
66+
const name = target.dataset.fsdocsTip;
67+
const unique = parseInt(target.dataset.fsdocsTipUnique, 10);
68+
hideTip(name);
69+
});
70+
4771
function Clipboard_CopyTo(value) {
4872
if (navigator.clipboard) {
4973
navigator.clipboard.writeText(value);
@@ -57,7 +81,4 @@ function Clipboard_CopyTo(value) {
5781
}
5882
}
5983

60-
window.showTip = showTip;
61-
window.hideTip = hideTip;
62-
// Used by API documentation
63-
window.Clipboard_CopyTo = Clipboard_CopyTo;
84+
window.Clipboard_CopyTo = Clipboard_CopyTo;

src/FSharp.Formatting.ApiDocs/GenerateHtml.fs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,9 @@ type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) =
4848
div [] [
4949
let id = UniqueID().ToString()
5050

51-
code
52-
[
53-
OnMouseOut(sprintf "hideTip(event, '%s', %s)" id id)
54-
OnMouseOver(sprintf "showTip(event, '%s', %s)" id id)
55-
]
56-
content
51+
code [ Custom("data-fsdocs-tip", id); Custom("data-fsdocs-tip-unique", id) ] content
5752

58-
div [ Class "fsdocs-tip"; Id id ] tip
53+
div [ Custom("popover", ""); Class "fsdocs-tip"; Id id ] tip
5954
]
6055

6156
let sourceLink url =

src/FSharp.Formatting.CodeFormat/HtmlFormatting.fs

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ type ToolTipFormatter(prefix) =
2323
let mutable count = 0
2424
let mutable uniqueId = 0
2525

26-
/// Formats tip and returns assignments for 'onmouseover' and 'onmouseout'
27-
member x.FormatTip (tip: ToolTipSpans) overlapping formatFunction =
26+
/// Formats tip and returns data attributes for tooltip triggering
27+
member x.FormatTip (tip: ToolTipSpans) formatFunction =
2828
uniqueId <- uniqueId + 1
2929

3030
let stringIndex =
@@ -34,32 +34,15 @@ type ToolTipFormatter(prefix) =
3434
count <- count + 1
3535
tips.Add(tip, (count, formatFunction tip))
3636
count
37-
// stringIndex is the index of the tool tip
38-
// uniqueId is globally unique id of the occurrence
39-
if overlapping then
40-
// The <span> may contain other <span>, so we need to
41-
// get the element and check where the mouse goes...
42-
String.Format(
43-
"id=\"{0}t{1}\" onmouseout=\"hideTip(event, '{0}{1}', {2})\" "
44-
+ "onmouseover=\"showTip(event, '{0}{1}', {2}, document.getElementById('{0}t{1}'))\" ",
45-
prefix,
46-
stringIndex,
47-
uniqueId
48-
)
49-
else
50-
String.Format(
51-
"onmouseout=\"hideTip(event, '{0}{1}', {2})\" "
52-
+ "onmouseover=\"showTip(event, '{0}{1}', {2})\" ",
53-
prefix,
54-
stringIndex,
55-
uniqueId
56-
)
37+
// stringIndex is the index of the tool tip div
38+
// uniqueId is the globally unique id of this hover occurrence
39+
String.Format("data-fsdocs-tip=\"{0}{1}\" data-fsdocs-tip-unique=\"{2}\" ", prefix, stringIndex, uniqueId)
5740

5841

5942
/// Returns all generated tool tip elements
6043
member x.WriteTipElements(writer: TextWriter) =
6144
for (KeyValue(_, (index, html))) in tips do
62-
writer.WriteLine(sprintf "<div class=\"fsdocs-tip\" id=\"%s%d\">%s</div>" prefix index html)
45+
writer.WriteLine(sprintf "<div popover class=\"fsdocs-tip\" id=\"%s%d\">%s</div>" prefix index html)
6346

6447

6548
/// Represents context used by the formatter
@@ -71,7 +54,7 @@ type FormattingContext =
7154
CloseTag: string
7255
OpenLinesTag: string
7356
CloseLinesTag: string
74-
FormatTip: ToolTipSpans -> bool -> (ToolTipSpans -> string) -> string
57+
FormatTip: ToolTipSpans -> (ToolTipSpans -> string) -> string
7558
TokenKindToCss: (TokenKind -> string) }
7659

7760
// --------------------------------------------------------------------------------------
@@ -106,12 +89,12 @@ let rec formatTokenSpans (ctx: FormattingContext) =
10689
| TokenSpan.Error(_kind, message, body) when ctx.GenerateErrors ->
10790
let tip = ToolTipReader.formatMultilineString (message.Trim().Split('\n'))
10891

109-
let tipAttributes = ctx.FormatTip tip true formatToolTipSpans
92+
let tipAttributes = ctx.FormatTip tip formatToolTipSpans
11093

11194
ctx.Writer.Write("<span ")
11295
ctx.Writer.Write(tipAttributes)
11396
ctx.Writer.Write("class=\"cerr\">")
114-
formatTokenSpans { ctx with FormatTip = fun _ _ _ -> "" } body
97+
formatTokenSpans { ctx with FormatTip = fun _ _ -> "" } body
11598
ctx.Writer.Write("</span>")
11699

117100
| TokenSpan.Error(_, _, body) -> formatTokenSpans ctx body
@@ -124,7 +107,7 @@ let rec formatTokenSpans (ctx: FormattingContext) =
124107
| TokenSpan.Omitted(body, hidden) ->
125108
let tip = ToolTipReader.formatMultilineString (hidden.Trim().Split('\n'))
126109

127-
let tipAttributes = ctx.FormatTip tip true formatToolTipSpans
110+
let tipAttributes = ctx.FormatTip tip formatToolTipSpans
128111

129112
ctx.Writer.Write("<span ")
130113
ctx.Writer.Write(tipAttributes)
@@ -136,7 +119,7 @@ let rec formatTokenSpans (ctx: FormattingContext) =
136119
// Generate additional attributes for ToolTip
137120
let tipAttributes =
138121
match tip with
139-
| Some(tip) -> ctx.FormatTip tip false formatToolTipSpans
122+
| Some(tip) -> ctx.FormatTip tip formatToolTipSpans
140123
| _ -> ""
141124

142125
// Get CSS class name of the token

tests/fsdocs-tool.Tests/ConvertCommandTests.fs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ let ``ConvertCommand omits fsdocs-tip divs when no template given`` () =
6666

6767
result |> shouldEqual 0
6868
let html = File.ReadAllText(outputFile)
69-
html |> shouldNotContainText "fsdocs-tip"
69+
// Tooltip trigger spans use data-fsdocs-tip attributes; the assertion checks that
70+
// the tooltip *div* elements (class="fsdocs-tip") are NOT emitted without a template.
71+
html |> shouldNotContainText "class=\"fsdocs-tip\""
7072

7173
[<Test>]
7274
let ``ConvertCommand converts .ipynb file to HTML`` () =

0 commit comments

Comments
 (0)