Skip to content

Commit b9bdaf9

Browse files
committed
Add tests
1 parent 1008fd9 commit b9bdaf9

File tree

4 files changed

+119
-57
lines changed

4 files changed

+119
-57
lines changed
Lines changed: 35 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { UAParser } from 'ua-parser-js'
2+
3+
const { getBrowser } = new UAParser()
4+
15
// Opens details elements (dropdowns) when navigating to an anchor link within them
26
// This enables deeplinking to collapsed dropdown content
37
export function openDetailsWithAnchor() {
@@ -13,27 +17,18 @@ export function openDetailsWithAnchor() {
1317
}
1418

1519
// Chrome automatically ensures parent content is visible, scroll immediately
16-
target.scrollIntoView({
17-
behavior: 'instant',
18-
block: 'start',
19-
})
20+
// Other browsers need manual scroll handling
21+
const browser = getBrowser()
22+
if (browser.name !== 'Chrome') {
23+
target.scrollIntoView({
24+
behavior: 'instant',
25+
block: 'start',
26+
})
27+
}
2028
}
2129
}
2230
}
2331

24-
// Updates the URL when a dropdown is manually opened/closed
25-
function updateUrlForDropdown(details: HTMLDetailsElement, isOpening: boolean) {
26-
const dropdownId = details.id
27-
if (!dropdownId) return
28-
29-
if (isOpening) {
30-
// Update URL to show the dropdown anchor (like clicking a heading link)
31-
window.history.pushState(null, '', `#${dropdownId}`)
32-
}
33-
// Note: We don't remove the hash when closing, just like headings don't
34-
// This keeps the URL consistent with how headings behave
35-
}
36-
3732
// Initialize the anchor handling functionality
3833
export function initOpenDetailsWithAnchor() {
3934
// Handle initial page load
@@ -42,41 +37,28 @@ export function initOpenDetailsWithAnchor() {
4237
// Handle hash changes within the same page (e.g., clicking anchor links)
4338
window.addEventListener('hashchange', openDetailsWithAnchor)
4439

45-
// Remove data-open-default on first click to enable URL updates
46-
document.addEventListener(
47-
'click',
48-
(event) => {
49-
const target = event.target as HTMLElement
50-
const dropdown = target.closest(
51-
'details.dropdown'
52-
) as HTMLDetailsElement
53-
if (dropdown && dropdown.dataset.openDefault === 'true') {
54-
delete dropdown.dataset.openDefault
55-
}
56-
},
57-
true
58-
)
59-
60-
// Handle manual dropdown toggling to update URL
61-
document.addEventListener(
62-
'toggle',
63-
(event) => {
64-
const target = event.target as HTMLElement
65-
66-
// Check if the target is a details element with dropdown class
67-
if (
68-
target.tagName === 'DETAILS' &&
69-
target.classList.contains('dropdown')
70-
) {
71-
const details = target as HTMLDetailsElement
72-
const isOpening = details.open
73-
74-
// Only update URL if NOT open by default (until first interaction)
75-
if (!details.dataset.openDefault) {
76-
updateUrlForDropdown(details, isOpening)
40+
// Handle dropdown URL updates
41+
document.addEventListener('click', (event) => {
42+
const target = event.target as HTMLElement
43+
const dropdown = target.closest('details.dropdown') as HTMLDetailsElement
44+
if (dropdown) {
45+
const initialState = dropdown.open
46+
47+
// Check state after toggle completes
48+
setTimeout(() => {
49+
const finalState = dropdown.open
50+
const stateChanged = initialState !== finalState
51+
52+
// If dropdown opened and doesn't have open-default flag, push URL
53+
if (stateChanged && finalState && !dropdown.dataset.openDefault) {
54+
window.history.pushState(null, '', `#${dropdown.id}`)
7755
}
78-
}
79-
},
80-
true
81-
)
56+
57+
// Remove open-default flag after first interaction
58+
if (dropdown.dataset.openDefault === 'true') {
59+
delete dropdown.dataset.openDefault
60+
}
61+
}, 10)
62+
}
63+
}, true)
8264
}

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Elastic.Markdown.Links.CrossLinks;
1313
using Elastic.Markdown.Myst;
1414
using Elastic.Markdown.Myst.Directives;
15+
using Elastic.Markdown.Myst.Directives.Admonition;
1516
using Elastic.Markdown.Myst.Directives.Include;
1617
using Elastic.Markdown.Myst.Directives.Stepper;
1718
using Elastic.Markdown.Myst.FrontMatter;
@@ -178,6 +179,7 @@ public async Task<MarkdownDocument> ParseFullAsync(Cancel ctx)
178179
_ = await MinimalParseAsync(ctx);
179180

180181
var document = await GetParseDocumentAsync(ctx);
182+
ValidateDropdownTitles(document);
181183
return document;
182184
}
183185

@@ -194,6 +196,34 @@ private IReadOnlyDictionary<string, string> GetSubstitutions()
194196
return allProperties;
195197
}
196198

199+
private void ValidateDropdownTitles(MarkdownDocument document)
200+
{
201+
var dropdowns = document.Descendants<DropdownBlock>().ToList();
202+
if (dropdowns.Count <= 1)
203+
return;
204+
205+
var titleGroups = dropdowns
206+
.GroupBy(d => d.Title, StringComparer.OrdinalIgnoreCase)
207+
.Where(g => g.Count() > 1);
208+
209+
foreach (var group in titleGroups)
210+
{
211+
var title = group.Key;
212+
foreach (var dropdown in group)
213+
{
214+
Collector.Write(new Diagnostic
215+
{
216+
Severity = Severity.Error,
217+
File = SourceFile.FullName,
218+
Line = dropdown.Line + 1,
219+
Column = dropdown.Column,
220+
Length = dropdown.OpeningLength,
221+
Message = $"Duplicate dropdown title '{title}' found. Each dropdown must have a unique title for proper anchor generation."
222+
});
223+
}
224+
}
225+
}
226+
197227
protected void ReadDocumentInstructions(MarkdownDocument document)
198228
{
199229
Title ??= document

src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
@inherits RazorSlice<Elastic.Markdown.Myst.Directives.Admonition.AdmonitionViewModel>
2-
<details class="dropdown @Model.Classes"
3-
id="@Model.CrossReferenceName"
4-
open="@Model.Open"
5-
data-open-default="@(Model.Open != null ? "true" : null)">
2+
@if (!string.IsNullOrEmpty(Model.Open))
3+
{
4+
<details class="dropdown @Model.Classes" id="@Model.CrossReferenceName" open="@Model.Open" data-open-default="true">
65
<summary class="dropdown-title">
76
<span class="sd-summary-text">@Model.Title</span>
87
<svg
@@ -19,3 +18,24 @@
1918
@Model.RenderBlock()
2019
</div>
2120
</details>
21+
}
22+
else
23+
{
24+
<details class="dropdown @Model.Classes" id="@Model.CrossReferenceName" open="@Model.Open">
25+
<summary class="dropdown-title">
26+
<span class="sd-summary-text">@Model.Title</span>
27+
<svg
28+
xmlns="http://www.w3.org/2000/svg"
29+
fill="none"
30+
viewBox="0 0 24 24"
31+
stroke-width="1.5"
32+
stroke="currentColor"
33+
class="w-4 mr-1 shrink -rotate-90 group-has-checked/label:rotate-0 cursor-pointer text-ink">
34+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/>
35+
</svg>
36+
</summary>
37+
<div class="dropdown-content">
38+
@Model.RenderBlock()
39+
</div>
40+
</details>
41+
}

tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using Elastic.Documentation.Diagnostics;
56
using Elastic.Markdown.Myst.Directives.Admonition;
7+
using Elastic.Markdown.Myst.Directives.Dropdown;
68
using FluentAssertions;
9+
using Markdig.Syntax;
710

811
namespace Elastic.Markdown.Tests.Directives;
912

@@ -123,3 +126,30 @@ A regular paragraph.
123126
[Fact]
124127
public void SetsCrossReferenceName() => Block!.CrossReferenceName.Should().Be("test-dropdown");
125128
}
129+
130+
public class DuplicateDropdownTitleTests(ITestOutputHelper output) : DirectiveTest(output,
131+
"""
132+
:::{dropdown} Same title
133+
First dropdown content
134+
:::
135+
136+
:::{dropdown} Same title
137+
Second dropdown content
138+
:::
139+
""")
140+
{
141+
[Fact]
142+
public void ReportsErrorForDuplicateDropdownTitles()
143+
{
144+
Collector.Diagnostics.Should().Contain(m =>
145+
m.Severity == Severity.Error &&
146+
m.Message.Contains("Duplicate dropdown title") &&
147+
m.Message.Contains("'Same title'"));
148+
149+
// Should report error for both duplicate dropdowns
150+
Collector.Diagnostics.Where(m =>
151+
m.Severity == Severity.Error &&
152+
m.Message.Contains("Duplicate dropdown title") &&
153+
m.Message.Contains("'Same title'")).Should().HaveCount(2);
154+
}
155+
}

0 commit comments

Comments
 (0)