diff --git a/docs/syntax/frontmatter.md b/docs/syntax/frontmatter.md index 43da89801..1a39426e4 100644 --- a/docs/syntax/frontmatter.md +++ b/docs/syntax/frontmatter.md @@ -9,27 +9,50 @@ In the frontmatter block, you can define the following fields: ```yaml --- navigation_title: This is the navigation title <1> -description: This is a description of the page <2> -applies_to: <3> +navigation_tooltip: This is a tooltip shown on hover <2> +description: This is a description of the page <3> +applies_to: <4> serverless: all -products: <4> +products: <5> - id: apm-agent - id: edot-sdk -sub: <5> - key: value +sub: <6> + key: value --- ``` 1. [`navigation_title`](#navigation-title) -2. [`description`](#description) -3. [`applies_to`](#applies-to) -4. [`products`](#products) -5. [`sub`](#subs) +2. [`navigation_tooltip`](#navigation-tooltip) +3. [`description`](#description) +4. [`applies_to`](#applies-to) +5. [`products`](#products) +6. [`sub`](#subs) ## Navigation Title See [](./titles.md) +## Navigation Tooltip + +Use the `navigation_tooltip` frontmatter to set custom tooltip text that appears when hovering over navigation items. + +The tooltip is displayed with a 500ms delay when hovering over navigation links in the sidebar and dropdown menus. +It's positioned dynamically relative to the viewport to avoid overflow issues. + +If you don't set a `navigation_tooltip`, it will automatically fall back to the `description` field. +This provides helpful context for users browsing the navigation without requiring additional configuration. + +The `navigation_tooltip` frontmatter is a string. Keep it concise (recommended 50-100 characters) for best readability. + +Example: + +```yaml +--- +navigation_title: Quick Start +navigation_tooltip: Learn how to set up and configure your first application in 5 minutes +--- +``` + ## Description Use the `description` frontmatter to set the description meta tag for a page. diff --git a/src/Elastic.ApiExplorer/ApiIndexLeafNavigation.cs b/src/Elastic.ApiExplorer/ApiIndexLeafNavigation.cs index b9e803aa7..cf5be4d21 100644 --- a/src/Elastic.ApiExplorer/ApiIndexLeafNavigation.cs +++ b/src/Elastic.ApiExplorer/ApiIndexLeafNavigation.cs @@ -19,6 +19,9 @@ public class ApiIndexLeafNavigation( /// public string NavigationTitle { get; } = navigationTitle; + /// + public string? NavigationTooltip => null; + /// public IRootNavigationItem NavigationRoot { get; } = rootNavigation; diff --git a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs index 0c21b61ec..1de224ff2 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs +++ b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs @@ -38,6 +38,8 @@ public class LandingNavigationItem : IApiGroupingNavigationItem Index.NavigationTitle; + public string? NavigationTooltip => null; // API landing items don't have tooltips + public LandingNavigationItem(string url) { NavigationRoot = this; @@ -73,6 +75,9 @@ INodeNavigationItem parent /// public abstract string NavigationTitle { get; } + /// + public string? NavigationTooltip => null; // API grouping items don't have tooltips + /// public IRootNavigationItem NavigationRoot { get; } = rootNavigation; @@ -134,6 +139,9 @@ public class EndpointNavigationItem(ApiEndpoint endpoint, IRootNavigationItem public string NavigationTitle { get; } = endpoint.Operations.First().ApiName; + /// + public string? NavigationTooltip => null; // API endpoint items don't have tooltips + /// public IRootNavigationItem NavigationRoot { get; } = rootNavigation; diff --git a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs index 679a17fcc..386d84a64 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs +++ b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs @@ -65,6 +65,8 @@ IApiGroupingNavigationItem parent public string NavigationTitle { get; } + public string? NavigationTooltip => null; // API operations don't have tooltips + public INodeNavigationItem? Parent { get; set; } public int NavigationIndex { get; set; } diff --git a/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs b/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs index 4f9c0c7f6..378ad38f6 100644 --- a/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Assembler/SiteNavigation.cs @@ -109,6 +109,9 @@ public SiteNavigation( /// public string NavigationTitle { get; } + /// + public string? NavigationTooltip => null; + /// public IRootNavigationItem NavigationRoot { get; } diff --git a/src/Elastic.Documentation.Navigation/IDocumentationFile.cs b/src/Elastic.Documentation.Navigation/IDocumentationFile.cs index 42c6a48c0..1eae85f7a 100644 --- a/src/Elastic.Documentation.Navigation/IDocumentationFile.cs +++ b/src/Elastic.Documentation.Navigation/IDocumentationFile.cs @@ -10,4 +10,7 @@ public interface IDocumentationFile : INavigationModel { /// Gets the title to display in navigation for this documentation file. string NavigationTitle { get; } + + /// Gets the tooltip text to display on hover for this documentation file in navigation. + string? NavigationTooltip { get; } } diff --git a/src/Elastic.Documentation.Navigation/INavigationItem.cs b/src/Elastic.Documentation.Navigation/INavigationItem.cs index 3a99e6f07..d71e62d54 100644 --- a/src/Elastic.Documentation.Navigation/INavigationItem.cs +++ b/src/Elastic.Documentation.Navigation/INavigationItem.cs @@ -20,6 +20,9 @@ public interface INavigationItem /// Gets the title displayed in navigation. string NavigationTitle { get; } + /// Gets the tooltip text displayed on hover for navigation items. + string? NavigationTooltip { get; } + /// Gets the root navigation item. IRootNavigationItem NavigationRoot { get; } diff --git a/src/Elastic.Documentation.Navigation/Isolated/Leaf/CrossLinkNavigationLeaf.cs b/src/Elastic.Documentation.Navigation/Isolated/Leaf/CrossLinkNavigationLeaf.cs index bca878c2b..a97ea2b4c 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Leaf/CrossLinkNavigationLeaf.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Leaf/CrossLinkNavigationLeaf.cs @@ -11,7 +11,11 @@ namespace Elastic.Documentation.Navigation.Isolated.Leaf; /// /// The URI pointing to the external resource /// The title to display in navigation -public record CrossLinkModel(Uri CrossLinkUri, string NavigationTitle) : IDocumentationFile; +public record CrossLinkModel(Uri CrossLinkUri, string NavigationTitle) : IDocumentationFile +{ + /// + public string? NavigationTooltip => null; +} [DebuggerDisplay("{Url}")] public class CrossLinkNavigationLeaf( @@ -41,6 +45,9 @@ INavigationHomeAccessor homeAccessor /// public string NavigationTitle => Model.NavigationTitle; + /// + public string? NavigationTooltip => Model.NavigationTooltip; + /// public int NavigationIndex { get; set; } diff --git a/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs b/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs index 55b17d3c3..d6da4a5f6 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Leaf/FileNavigationLeaf.cs @@ -74,6 +74,9 @@ string DetermineUrl() /// public string NavigationTitle => Model.NavigationTitle; + /// + public string? NavigationTooltip => Model.NavigationTooltip; + /// public int NavigationIndex { get; set; } diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs index e23e05bcd..08921009f 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/DocumentationSetNavigation.cs @@ -117,6 +117,9 @@ public DocumentationSetNavigation( /// public string NavigationTitle => Index.NavigationTitle; + /// + public string? NavigationTooltip => Index.NavigationTooltip; + public IRootNavigationItem NavigationRoot => HomeProvider == this ? field : HomeProvider.NavigationRoot; diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/FolderNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/FolderNavigation.cs index 13f2b18e7..30e8f2ef9 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/FolderNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/FolderNavigation.cs @@ -25,6 +25,9 @@ public class FolderNavigation( /// public string NavigationTitle => Index.NavigationTitle; + /// + public string? NavigationTooltip => Index.NavigationTooltip; + /// public IRootNavigationItem NavigationRoot => homeAccessor.HomeProvider.NavigationRoot; diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs index 07349fc7a..0a43c1220 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/TableOfContentsNavigation.cs @@ -58,6 +58,9 @@ INavigationHomeProvider homeProvider /// public string NavigationTitle => Index.NavigationTitle; + /// + public string? NavigationTooltip => Index.NavigationTooltip; + /// /// TableOfContentsNavigation's NavigationRoot comes from its HomeProvider. /// According to url-building.md: "In isolated builds the NavigationRoot is always the DocumentationSetNavigation" diff --git a/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs b/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs index 5e5e17904..630532be1 100644 --- a/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs +++ b/src/Elastic.Documentation.Navigation/Isolated/Node/VirtualFileNavigation.cs @@ -21,6 +21,9 @@ public class VirtualFileNavigation(TModel model, IFileInfo fileInfo, Vir /// public string NavigationTitle => Index.NavigationTitle; + /// + public string? NavigationTooltip => Index.NavigationTooltip; + /// public IRootNavigationItem NavigationRoot => args.HomeAccessor.HomeProvider.NavigationRoot; diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index f5079d744..7c5087f89 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -3,6 +3,7 @@ import { initCopyButton } from './copybutton' import { initHighlight } from './hljs' import { initImageCarousel } from './image-carousel' import './markdown/applies-to' +import { initNavigationTooltips } from './navigation-tooltip' import { openDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' @@ -81,6 +82,7 @@ document.addEventListener('htmx:load', function (event: HtmxEvent) { initTabs() initAppliesSwitch() initMath() + initNavigationTooltips() // We do this so that the navigation is not initialized twice if (isLazyLoadNavigationEnabled) { diff --git a/src/Elastic.Documentation.Site/Assets/navigation-tooltip.css b/src/Elastic.Documentation.Site/Assets/navigation-tooltip.css new file mode 100644 index 000000000..854cb1210 --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/navigation-tooltip.css @@ -0,0 +1,75 @@ +/* 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 + */ + +/** + * Navigation Tooltip Styles + * Viewport-positioned tooltips for navigation items + */ + +.nav-tooltip { + position: fixed; + padding: 8px 12px; + background-color: #1a1c21; + color: #ffffff; + font-size: 13px; + line-height: 1.4; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + max-width: 320px; + word-wrap: break-word; + white-space: normal; + opacity: 0; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; + pointer-events: none; + z-index: 10000; +} + +.nav-tooltip--visible { + opacity: 1; + visibility: visible; +} + +/* Arrow indicator pointing to the nav item */ +.nav-tooltip::before { + content: ''; + position: absolute; + left: -6px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-style: solid; + border-width: 6px 6px 6px 0; + border-color: transparent #1a1c21 transparent transparent; +} + +/* Light theme support */ +@media (prefers-color-scheme: light) { + .nav-tooltip { + background-color: #343741; + color: #ffffff; + } + + .nav-tooltip::before { + border-color: transparent #343741 transparent transparent; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .nav-tooltip { + border: 1px solid currentColor; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .nav-tooltip { + transition: none; + } +} diff --git a/src/Elastic.Documentation.Site/Assets/navigation-tooltip.ts b/src/Elastic.Documentation.Site/Assets/navigation-tooltip.ts new file mode 100644 index 000000000..3945cc14d --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/navigation-tooltip.ts @@ -0,0 +1,203 @@ +// 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 + +/** + * Navigation Tooltip System + * Dynamically positions tooltips relative to the viewport for navigation items + */ + +interface TooltipOptions { + offsetX: number + offsetY: number + delay: number +} + +class NavigationTooltip { + private tooltip: HTMLElement | null = null + private currentTarget: HTMLElement | null = null + private showTimer: number | null = null + private hideTimer: number | null = null + private readonly options: TooltipOptions + + constructor(options: Partial = {}) { + this.options = { + offsetX: options.offsetX ?? 12, + offsetY: options.offsetY ?? 0, + delay: options.delay ?? 500, + } + } + + private createTooltip(): HTMLElement { + const tooltip = document.createElement('div') + tooltip.className = 'nav-tooltip' + tooltip.setAttribute('role', 'tooltip') + tooltip.style.position = 'fixed' + tooltip.style.pointerEvents = 'none' + tooltip.style.zIndex = '10000' + document.body.appendChild(tooltip) + return tooltip + } + + private getTooltip(): HTMLElement { + if (!this.tooltip) { + this.tooltip = this.createTooltip() + } + return this.tooltip + } + + private positionTooltip(target: HTMLElement): void { + const tooltip = this.getTooltip() + const rect = target.getBoundingClientRect() + + // Position tooltip to the right of the nav item + const left = rect.right + this.options.offsetX + const top = rect.top + rect.height / 2 + this.options.offsetY + + tooltip.style.left = `${left}px` + tooltip.style.top = `${top}px` + tooltip.style.transform = 'translateY(-50%)' + + // Check if tooltip goes off the right edge of viewport + const tooltipRect = tooltip.getBoundingClientRect() + if (tooltipRect.right > window.innerWidth) { + // Position to the left instead + tooltip.style.left = `${rect.left - tooltipRect.width - this.options.offsetX}px` + } + + // Check if tooltip goes off the bottom edge + if (tooltipRect.bottom > window.innerHeight) { + const newTop = window.innerHeight - tooltipRect.height - 8 + tooltip.style.top = `${newTop}px` + tooltip.style.transform = 'none' + } + + // Check if tooltip goes off the top edge + if (tooltipRect.top < 0) { + tooltip.style.top = '8px' + tooltip.style.transform = 'none' + } + } + + private showTooltip(target: HTMLElement, text: string): void { + this.currentTarget = target + const tooltip = this.getTooltip() + tooltip.textContent = text + tooltip.classList.add('nav-tooltip--visible') + this.positionTooltip(target) + } + + private hideTooltip(): void { + if (this.tooltip) { + this.tooltip.classList.remove('nav-tooltip--visible') + } + this.currentTarget = null + } + + private handleMouseEnter = (e: MouseEvent): void => { + const target = e.currentTarget as HTMLElement + const tooltipText = target.getAttribute('data-nav-tooltip') + + if (!tooltipText) return + + // Clear any pending hide timer + if (this.hideTimer !== null) { + clearTimeout(this.hideTimer) + this.hideTimer = null + } + + // Set a timer to show the tooltip after delay + this.showTimer = window.setTimeout(() => { + this.showTooltip(target, tooltipText) + }, this.options.delay) + } + + private handleMouseLeave = (): void => { + // Clear show timer if mouse leaves before delay completes + if (this.showTimer !== null) { + clearTimeout(this.showTimer) + this.showTimer = null + } + + // Hide tooltip after a short delay + this.hideTimer = window.setTimeout(() => { + this.hideTooltip() + }, 100) + } + + private handleScroll = (): void => { + // Reposition tooltip if it's visible and target still exists + if ( + this.currentTarget && + this.tooltip?.classList.contains('nav-tooltip--visible') + ) { + this.positionTooltip(this.currentTarget) + } + } + + public init(): void { + // Find all navigation items with tooltips + const navItems = + document.querySelectorAll('[data-nav-tooltip]') + + navItems.forEach((item) => { + item.addEventListener('mouseenter', this.handleMouseEnter) + item.addEventListener('mouseleave', this.handleMouseLeave) + }) + + // Update tooltip position on scroll + window.addEventListener('scroll', this.handleScroll, { passive: true }) + + // Update tooltip position on resize + window.addEventListener('resize', this.handleScroll, { passive: true }) + } + + public destroy(): void { + const navItems = + document.querySelectorAll('[data-nav-tooltip]') + + navItems.forEach((item) => { + item.removeEventListener('mouseenter', this.handleMouseEnter) + item.removeEventListener('mouseleave', this.handleMouseLeave) + }) + + window.removeEventListener('scroll', this.handleScroll) + window.removeEventListener('resize', this.handleScroll) + + if (this.showTimer !== null) { + clearTimeout(this.showTimer) + } + + if (this.hideTimer !== null) { + clearTimeout(this.hideTimer) + } + + if (this.tooltip) { + this.tooltip.remove() + this.tooltip = null + } + } +} + +// Store global instance +let globalNavTooltip: NavigationTooltip | null = null + +// Initialize navigation tooltips +export function initNavigationTooltips(): void { + // Clean up previous instance if it exists + if (globalNavTooltip) { + globalNavTooltip.destroy() + } + + globalNavTooltip = new NavigationTooltip() + globalNavTooltip.init() +} + +// Initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initNavigationTooltips) +} else { + initNavigationTooltips() +} + +export { NavigationTooltip } diff --git a/src/Elastic.Documentation.Site/Assets/styles.css b/src/Elastic.Documentation.Site/Assets/styles.css index fb3bbf8a6..beaec84b3 100644 --- a/src/Elastic.Documentation.Site/Assets/styles.css +++ b/src/Elastic.Documentation.Site/Assets/styles.css @@ -12,6 +12,7 @@ @import './markdown/icons.css'; @import './markdown/kbd.css'; @import './copybutton.css'; +@import './navigation-tooltip.css'; @import './markdown/admonition.css'; @import './markdown/dropdown.css'; @import './markdown/table.css'; diff --git a/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml b/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml index 42b593a21..3f8ed6519 100644 --- a/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml +++ b/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml @@ -14,7 +14,8 @@ + @Htmx.GetNavHxAttributes(true) + data-nav-tooltip="@currentTopLevelItem.NavigationTooltip"> @currentTopLevelItem.NavigationTitle @@ -38,7 +39,8 @@ + @Htmx.GetNavHxAttributes(false, "mouseover") + data-nav-tooltip="@item.NavigationTooltip"> @item.NavigationTitle @@ -53,7 +55,8 @@ + class="inline-block mx-4 mt-6 font-semibold text-ink hover:text-black" + data-nav-tooltip="@Model.Tree.NavigationTooltip"> @Model.Title } diff --git a/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml b/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml index 5a64551fd..9eef5aaa1 100644 --- a/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml +++ b/src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml @@ -24,6 +24,7 @@ href="@group.Url" @Htmx.GetNavHxAttributes(Model.IsPrimaryNavEnabled && group.NavigationRoot.Id == Model.RootNavigationId || true) class="sidebar-link group-[.current]/li:text-blue-elastic!" + data-nav-tooltip="@group.NavigationTooltip" > @group.NavigationTitle @@ -38,7 +39,8 @@ + class="sidebar-link pr-2 content-center @(isTopLevel ? "font-semibold" : "") group-[.current]/li:text-blue-elastic!" + data-nav-tooltip="@g.NavigationTooltip"> @(g.NavigationTitle) @if (!allHidden) @@ -93,6 +95,7 @@ href="@leaf.Url" @Htmx.GetNavHxAttributes(hasSameTopLevelGroup) class="sidebar-link grow group-[.current]/li:text-blue-elastic!" + data-nav-tooltip="@leaf.NavigationTooltip" > @leaf.NavigationTitle diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 07f7bf7d0..d65398033 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -78,6 +78,23 @@ public string NavigationTitle private set => field = value.StripMarkdown(); } + public string? NavigationTooltip + { + get + { + if (!string.IsNullOrEmpty(field)) + return field; + + var description = YamlFrontMatter?.Description; + if (string.IsNullOrEmpty(description)) + return null; + + // Strip markdown and replace quotes to prevent HTML attribute issues + return description.StripMarkdown().Replace("\"", "'"); + } + private set => field = value?.StripMarkdown().Replace("\"", "'"); + } + //indexed by slug private readonly Dictionary _pageTableOfContent = new(StringComparer.OrdinalIgnoreCase); @@ -138,11 +155,14 @@ private IReadOnlyDictionary GetSubstitutions() var globalSubstitutions = _globalSubstitutions; var fileSubstitutions = YamlFrontMatter?.Properties; if (fileSubstitutions is not { Count: >= 0 }) - return globalSubstitutions; + return globalSubstitutions ?? new Dictionary(); var allProperties = new Dictionary(fileSubstitutions); - foreach (var (key, value) in globalSubstitutions) - allProperties[key] = value; + if (globalSubstitutions is not null) + { + foreach (var (key, value) in globalSubstitutions) + allProperties[key] = value; + } return allProperties; } @@ -156,6 +176,8 @@ protected void ReadDocumentInstructions(MarkdownDocument document, Func? Properties { get; set; } diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs index 87548cf92..b891e4023 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs @@ -336,3 +336,102 @@ public void HasErrorsForNotAbsoluteUri() Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Invalid mapped_pages URL: \"not-a-uri-at-all\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"")); } } + +public class NavigationTooltipExplicit(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + navigation_tooltip: "This is a custom tooltip" + --- + + # Test Page + """ +) +{ + [Fact] + public void ReadsNavigationTooltip() => File.NavigationTooltip.Should().Be("This is a custom tooltip"); +} + +public class NavigationTooltipFallbackToDescription(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + description: "This is a description" + --- + + # Test Page + """ +) +{ + [Fact] + public void ReadsNavigationTooltipFromDescription() => File.NavigationTooltip.Should().Be("This is a description"); +} + +public class NavigationTooltipNullWhenNeitherExists(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + --- + + # Test Page + """ +) +{ + [Fact] + public void ReadsNavigationTooltipAsNull() => File.NavigationTooltip.Should().BeNull(); +} + +public class NavigationTooltipWithDoubleQuotes(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + navigation_tooltip: "Learn about \"elastic solutions\" here" + --- + + # Test Page + """ +) +{ + [Fact] + public void ReplacesDoubleQuotesWithSingleQuotes() => File.NavigationTooltip.Should().Be("Learn about 'elastic solutions' here"); +} + +public class NavigationTooltipDescriptionWithDoubleQuotes(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + description: "This is a \"description\" with quotes" + --- + + # Test Page + """ +) +{ + [Fact] + public void ReplacesDoubleQuotesInDescriptionFallback() => File.NavigationTooltip.Should().Be("This is a 'description' with quotes"); +} + +public class NavigationTooltipStripsMarkdown(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + navigation_tooltip: "This has **bold** and *italic* text" + --- + + # Test Page + """ +) +{ + [Fact] + public void StripsMarkdownFromTooltip() => File.NavigationTooltip.Should().Be("This has bold and italic text"); +} + +public class NavigationTooltipSupportsSubstitutions(ITestOutputHelper output) : DirectiveTest(output, + """ + --- + navigation_tooltip: "Guide for {{product}}" + sub: + product: "Elasticsearch" + --- + + # Test Page + """ +) +{ + [Fact] + public void ReplacesSubstitutionsInTooltip() => File.NavigationTooltip.Should().Be("Guide for Elasticsearch"); +} diff --git a/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs b/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs index 6ee67d23c..40144c506 100644 --- a/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs @@ -173,6 +173,7 @@ private sealed class LeafNavigationItemStub(RootNavigationItemStub root) : ILeaf { public string Url => "/"; public string NavigationTitle => "Root"; + public string? NavigationTooltip => null; public IRootNavigationItem NavigationRoot { get; } = root; public INodeNavigationItem? Parent { get; set; } public bool Hidden => false; @@ -184,6 +185,7 @@ private sealed class LeafNavigationItemStub(RootNavigationItemStub root) : ILeaf public string Url => "/"; public string NavigationTitle => "Root"; + public string? NavigationTooltip => null; public IRootNavigationItem NavigationRoot => this; public INodeNavigationItem? Parent { get; set; } public bool Hidden => false; @@ -200,6 +202,7 @@ private sealed class LeafNavigationItemStub(RootNavigationItemStub root) : ILeaf public string Url { get; } = url; public string NavigationTitle => "Stub"; + public string? NavigationTooltip => null; public IRootNavigationItem NavigationRoot => Root; public INodeNavigationItem? Parent { get; set; } public bool Hidden => false; diff --git a/tests/Navigation.Tests/TestDocumentationSetContext.cs b/tests/Navigation.Tests/TestDocumentationSetContext.cs index d7fb32605..3c590abac 100644 --- a/tests/Navigation.Tests/TestDocumentationSetContext.cs +++ b/tests/Navigation.Tests/TestDocumentationSetContext.cs @@ -114,6 +114,9 @@ public class TestDocumentationFile(string navigationTitle) : IDocumentationFile { /// public string NavigationTitle { get; } = navigationTitle; + + /// + public string? NavigationTooltip => null; } public class TestDocumentationFileFactory : IDocumentationFileFactory