From b38854e8c5a68ee80f35daf28a9fd3798cafe6c8 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Thu, 3 Apr 2025 23:58:50 -0500 Subject: [PATCH 01/22] Unified where the class mud-popover-provider comes from --- src/MudBlazor/Components/Popover/MudPopoverProvider.razor | 2 +- src/MudBlazor/Services/Popover/PopoverOptions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MudBlazor/Components/Popover/MudPopoverProvider.razor b/src/MudBlazor/Components/Popover/MudPopoverProvider.razor index ed3a4d69a787..009ebf662e1e 100644 --- a/src/MudBlazor/Components/Popover/MudPopoverProvider.razor +++ b/src/MudBlazor/Components/Popover/MudPopoverProvider.razor @@ -2,7 +2,7 @@ @if (Enabled) { -
+
@foreach (var handler in PopoverService.ActivePopovers) { diff --git a/src/MudBlazor/Services/Popover/PopoverOptions.cs b/src/MudBlazor/Services/Popover/PopoverOptions.cs index aeb767338f7e..8a79147b29e3 100644 --- a/src/MudBlazor/Services/Popover/PopoverOptions.cs +++ b/src/MudBlazor/Services/Popover/PopoverOptions.cs @@ -20,7 +20,7 @@ public class PopoverOptions /// Gets or sets the CSS class of the popover container. /// The default value is mudblazor-main-content. /// - public string ContainerClass { get; set; } = "mudblazor-main-content"; + public string ContainerClass { get; set; } = "mud-popover-provider"; /// /// Gets or sets the FlipMargin for the popover. From f780f3adf863a1b98e33052c0db19919fcec6b80 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Thu, 3 Apr 2025 23:59:29 -0500 Subject: [PATCH 02/22] Enhance popover functionality and cleanup code Updated `window.mudpopoverHelper` with new properties for better popover behavior. Improved `MudPopover` class initialization with error handling and a `MutationObserver`. Refactored `initialize` and `connect` methods to streamline observer management and removed unnecessary scroll and resize listeners. Simplified `dispose` method for better maintainability. --- src/MudBlazor/TScripts/mudPopover.js | 188 +++++++++++++-------------- 1 file changed, 92 insertions(+), 96 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 318c46e43e0c..f45dfb3ca564 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -3,6 +3,56 @@ // See the LICENSE file in the project root for more information. window.mudpopoverHelper = { + // set by the class MudPopover + mainContainerClass: null, + + flipMargin: 0, + + flipClassReplacements: { + 'top': { + 'mud-popover-top-left': 'mud-popover-bottom-left', + 'mud-popover-top-center': 'mud-popover-bottom-center', + 'mud-popover-anchor-bottom-center': 'mud-popover-anchor-top-center', + 'mud-popover-top-right': 'mud-popover-bottom-right', + }, + 'left': { + 'mud-popover-top-left': 'mud-popover-top-right', + 'mud-popover-center-left': 'mud-popover-center-right', + 'mud-popover-anchor-center-right': 'mud-popover-anchor-center-left', + 'mud-popover-bottom-left': 'mud-popover-bottom-right', + }, + 'right': { + 'mud-popover-top-right': 'mud-popover-top-left', + 'mud-popover-center-right': 'mud-popover-center-left', + 'mud-popover-anchor-center-left': 'mud-popover-anchor-center-right', + 'mud-popover-bottom-right': 'mud-popover-bottom-left', + }, + 'bottom': { + 'mud-popover-bottom-left': 'mud-popover-top-left', + 'mud-popover-bottom-center': 'mud-popover-top-center', + 'mud-popover-anchor-top-center': 'mud-popover-anchor-bottom-center', + 'mud-popover-bottom-right': 'mud-popover-top-right', + }, + 'top-and-left': { + 'mud-popover-top-left': 'mud-popover-bottom-right', + }, + 'top-and-right': { + 'mud-popover-top-right': 'mud-popover-bottom-left', + }, + 'bottom-and-left': { + 'mud-popover-bottom-left': 'mud-popover-top-right', + }, + 'bottom-and-right': { + 'mud-popover-bottom-right': 'mud-popover-top-left', + }, + + }, + + basePopoverZIndex: parseInt(getComputedStyle(document.documentElement) + .getPropertyValue('--mud-zindex-popover')) || 1200, + + baseTooltipZIndex: parseInt(getComputedStyle(document.documentElement) + .getPropertyValue('--mud-zindex-tooltip')) || 1600, debounce: function (func, wait) { let timeout; @@ -104,54 +154,6 @@ window.mudpopoverHelper = { }; }, - flipClassReplacements: { - 'top': { - 'mud-popover-top-left': 'mud-popover-bottom-left', - 'mud-popover-top-center': 'mud-popover-bottom-center', - 'mud-popover-anchor-bottom-center': 'mud-popover-anchor-top-center', - 'mud-popover-top-right': 'mud-popover-bottom-right', - }, - 'left': { - 'mud-popover-top-left': 'mud-popover-top-right', - 'mud-popover-center-left': 'mud-popover-center-right', - 'mud-popover-anchor-center-right': 'mud-popover-anchor-center-left', - 'mud-popover-bottom-left': 'mud-popover-bottom-right', - }, - 'right': { - 'mud-popover-top-right': 'mud-popover-top-left', - 'mud-popover-center-right': 'mud-popover-center-left', - 'mud-popover-anchor-center-left': 'mud-popover-anchor-center-right', - 'mud-popover-bottom-right': 'mud-popover-bottom-left', - }, - 'bottom': { - 'mud-popover-bottom-left': 'mud-popover-top-left', - 'mud-popover-bottom-center': 'mud-popover-top-center', - 'mud-popover-anchor-top-center': 'mud-popover-anchor-bottom-center', - 'mud-popover-bottom-right': 'mud-popover-top-right', - }, - 'top-and-left': { - 'mud-popover-top-left': 'mud-popover-bottom-right', - }, - 'top-and-right': { - 'mud-popover-top-right': 'mud-popover-bottom-left', - }, - 'bottom-and-left': { - 'mud-popover-bottom-left': 'mud-popover-top-right', - }, - 'bottom-and-right': { - 'mud-popover-bottom-right': 'mud-popover-top-left', - }, - - }, - - flipMargin: 0, - - basePopoverZIndex: parseInt(getComputedStyle(document.documentElement) - .getPropertyValue('--mud-zindex-popover')) || 1200, - - baseTooltipZIndex: parseInt(getComputedStyle(document.documentElement) - .getPropertyValue('--mud-zindex-tooltip')) || 1600, - getPositionForFlippedPopver: function (inputArray, selector, boundingRect, selfRect) { const classList = []; for (var i = 0; i < inputArray.length; i++) { @@ -384,29 +386,6 @@ window.mudpopoverHelper = { } }, - popoverScrollListener: function (node) { - let currentNode = node.parentNode; - while (currentNode) { - //console.log(currentNode); - const isScrollable = - (currentNode.scrollHeight > currentNode.clientHeight) || // Vertical scroll - (currentNode.scrollWidth > currentNode.clientWidth); // Horizontal scroll - if (isScrollable) { - //console.log("scrollable"); - currentNode.addEventListener('scroll', () => { - //console.log("scrolled"); - window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); - window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); - }); - } - // Stop if we reach the body, or head - if (currentNode.tagName === "BODY") { - break; - } - currentNode = currentNode.parentNode; - } - }, - placePopoverByClassSelector: function (classSelector = null) { var items = window.mudPopover.getAllObservedContainers(); @@ -423,7 +402,7 @@ window.mudpopoverHelper = { }, countProviders: function () { - return document.querySelectorAll(".mud-popover-provider").length; + return document.querySelectorAll(`.${window.mudpopoverHelper.mainContainerClass}`).length; }, updatePopoverOverlay: function (popoverContentNode) { @@ -432,7 +411,7 @@ window.mudpopoverHelper = { return; } // set any associated overlay to equal z-index - const provider = popoverContentNode.closest('.mud-popover-provider'); + const provider = popoverContentNode.closest(`.${window.mudpopoverHelper.mainContainerClass}`); if (provider && popoverContentNode.classList.contains("mud-popover")) { const overlay = provider.querySelector('.mud-overlay'); // skip any overlay marked with mud-skip-overlay @@ -508,7 +487,6 @@ class MudPopover { constructor() { this.map = {}; this.contentObserver = null; - this.mainContainerClass = null; } callback(id, mutationsList, observer) { @@ -596,39 +574,57 @@ class MudPopover { } } - initialize(containerClass, flipMargin) { + initialize(containerClass, flipMargin) { + // only happens when the PopoverService is created which happens on application start and anytime the service might crash const mainContent = document.getElementsByClassName(containerClass); if (mainContent.length == 0) { + console.error(`No Popover Container found with class ${containerClass}`); return; } + // store options from PopoverOptions in mudpopoverHelper + window.mudpopoverHelper.mainContainerClass = containerClass; if (flipMargin) { window.mudpopoverHelper.flipMargin = flipMargin; } + // create a single observer to watch all popovers in the provider + const provider = mainContent[0]; + + // options to observe for + const config = { + attributes: true, // only observe attributes + subtree: true, // all descendants of popover + attributeFilter: ['class', 'data-ticks'] + }; - this.mainContainerClass = containerClass; - - if (!mainContent[0].mudPopoverMark) { - mainContent[0].mudPopoverMark = "mudded"; - if (this.contentObserver != null) { - this.contentObserver.disconnect(); - this.contentObserver = null; + const observer = new MutationObserver((mutations) => { + for (const mutation in mutations) { + // if it's direct parent is the provider + // and contains the class mud-popover + if (mutation.target.parentNode === provider && mutation.target.classList.contains('mud-popover')) { + console.log('Change detected on popover element:', mutation.target.id); + this.callback.bind(this, mutation.target.id) + } } + }); - this.contentObserver = new ResizeObserver(entries => { - window.mudpopoverHelper.placePopoverByClassSelector(); - }); - - this.contentObserver.observe(mainContent[0]); - } + observer.observe(provider, config); + // store it so we can dispose of it properly + this.contentObserver = observer; } connect(id) { - this.initialize(this.mainContainerClass); + // this happens when a popover is created in the dom (not necessarily displayed) + // removed extra initialize and extra scroll listener that attached to the provider and body for every popover + // this is the origin of the popover in the dom, it can be nested inside another popover's content + // e.g. the filter popover for datagrid, this would be the inside of where the mudpopover was placed + // popoverNode.parentNode is it's immediate parent or the actual element in the above example const popoverNode = document.getElementById('popover-' + id); - mudpopoverHelper.popoverScrollListener(popoverNode); + + // this is the content node in the provider regardless of the RenderFragment that exists when the popover is active const popoverContentNode = document.getElementById('popovercontent-' + id); + if (popoverNode && popoverNode.parentNode && popoverContentNode) { window.mudpopoverHelper.placePopover(popoverNode); @@ -637,7 +633,7 @@ class MudPopover { const observer = new MutationObserver(this.callback.bind(this, id)); - observer.observe(popoverContentNode, config); + // observer.observe(popoverContentNode, config); const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { @@ -650,7 +646,7 @@ class MudPopover { } }); - resizeObserver.observe(popoverNode.parentNode); + // resizeObserver.observe(popoverNode.parentNode); const contentNodeObserver = new ResizeObserver(entries => { for (let entry of entries) { @@ -660,7 +656,7 @@ class MudPopover { } }); - contentNodeObserver.observe(popoverContentNode); + // contentNodeObserver.observe(popoverContentNode); this.map[id] = { popoverContentNode: popoverContentNode, @@ -684,9 +680,9 @@ class MudPopover { } dispose() { - for (var i in this.map) { - disconnect(i); - } + // for (var i in this.map) { + // disconnect(i); + // } this.contentObserver.disconnect(); this.contentObserver = null; From 964a66c38bbb452c3bf262ae3427f4148f16b59d Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Fri, 4 Apr 2025 16:52:12 -0500 Subject: [PATCH 03/22] save to travel home still optimizing flip methods --- src/MudBlazor/Styles/components/_popover.scss | 2 + src/MudBlazor/TScripts/mudPopover.js | 370 +++++++++--------- 2 files changed, 197 insertions(+), 175 deletions(-) diff --git a/src/MudBlazor/Styles/components/_popover.scss b/src/MudBlazor/Styles/components/_popover.scss index 8294cff79cea..0975151cdc1f 100644 --- a/src/MudBlazor/Styles/components/_popover.scss +++ b/src/MudBlazor/Styles/components/_popover.scss @@ -3,6 +3,8 @@ z-index: calc(var(--mud-zindex-popover) + 1); position: absolute; opacity: 0; + top: -9999px; + left: -9999px; &.mud-popover-fixed { position: fixed; diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index f45dfb3ca564..a75c70e52a39 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -3,11 +3,32 @@ // See the LICENSE file in the project root for more information. window.mudpopoverHelper = { - // set by the class MudPopover + // set by the class MudPopover in initialize mainContainerClass: null, + // set by the class MudPopover in initialize flipMargin: 0, + // used for setting a debounce + debounce: function (func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + basePopoverZIndex: parseInt(getComputedStyle(document.documentElement) + .getPropertyValue('--mud-zindex-popover')) || 1200, + + baseTooltipZIndex: parseInt(getComputedStyle(document.documentElement) + .getPropertyValue('--mud-zindex-tooltip')) || 1600, + + // static set of replacement valus flipClassReplacements: { 'top': { 'mud-popover-top-left': 'mud-popover-bottom-left', @@ -48,37 +69,7 @@ window.mudpopoverHelper = { }, - basePopoverZIndex: parseInt(getComputedStyle(document.documentElement) - .getPropertyValue('--mud-zindex-popover')) || 1200, - - baseTooltipZIndex: parseInt(getComputedStyle(document.documentElement) - .getPropertyValue('--mud-zindex-tooltip')) || 1600, - - debounce: function (func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }, - - rafThrottle: function (func) { - let ticking = false; - return function (...args) { - if (!ticking) { - window.requestAnimationFrame(() => { - func.apply(this, args); - ticking = false; - }); - ticking = true; - } - }; - }, - + // used to calculate the position of the popover calculatePopoverPosition: function (list, boundingRect, selfRect) { let top = 0; let left = 0; @@ -149,27 +140,33 @@ window.mudpopoverHelper = { offsetY = -selfRect.height; } + console.log(`top: ${top}, left: ${left}, offsetX: ${offsetX}, offsetY: ${offsetY}`); return { top: top, left: left, offsetX: offsetX, offsetY: offsetY }; }, + // used to flip the popover using the flipClassReplacements, so we pass it the flip direction by selector + // with a list of classes and returns the proper flipped position for calculatePopoverPosition getPositionForFlippedPopver: function (inputArray, selector, boundingRect, selfRect) { const classList = []; + const replacementsList = {}; for (var i = 0; i < inputArray.length; i++) { const item = inputArray[i]; - const replacments = window.mudpopoverHelper.flipClassReplacements[selector][item]; - if (replacments) { - classList.push(replacments); + const replacements = window.mudpopoverHelper.flipClassReplacements[selector][item]; + if (replacements) { + replacementsList[item] = replacements; + classList.push(replacements); } else { classList.push(item); } } - + console.log(replacementsList); return window.mudpopoverHelper.calculatePopoverPosition(classList, boundingRect, selfRect); }, + // primary positioning method placePopover: function (popoverNode, classSelector) { // parentNode is the calling element, mudmenu/tooltip/etc not the parent popover if it's a child popover // this happens at page load unless it's popover inside a popover, then it happens when you activate the parent @@ -178,15 +175,19 @@ window.mudpopoverHelper = { const id = popoverNode.id.substr(8); const popoverContentNode = document.getElementById('popovercontent-' + id); + // if the popover doesn't exist we stop if (!popoverContentNode) { return; } + const classList = popoverContentNode.classList; + // if the popover isn't open we stop if (classList.contains('mud-popover-open') == false) { return; } + // if a classSelector was supplied and doesn't exist we stop if (classSelector) { if (classList.contains(classSelector) == false) { return; @@ -206,11 +207,13 @@ window.mudpopoverHelper = { const selfRect = popoverContentNode.getBoundingClientRect(); const classListArray = Array.from(classList); + // calculate position based on opening anchor/transform const postion = window.mudpopoverHelper.calculatePopoverPosition(classListArray, boundingRect, selfRect); let left = postion.left; let top = postion.top; let offsetX = postion.offsetX; let offsetY = postion.offsetY; + // get the top/left/ from popoverContentNode if the popover has been hardcoded for position if (classList.contains('mud-popover-position-override')) { left = parseInt(popoverContentNode.style['left']) || left; @@ -227,9 +230,20 @@ window.mudpopoverHelper = { width: selfRect.width, height: selfRect.height }; - } + } + // flipping logic - if (classList.contains('mud-popover-overflow-flip-onopen') || classList.contains('mud-popover-overflow-flip-always')) { + // get flip status + let selector = popoverContentNode.getAttribute("data-mudpopover-flip"); + console.log(`starting: ${selector}`); + // if there has not been a flip and it's got an onopen class OR + // there is a flip-always class + if ((!selector && classList.contains('mud-popover-overflow-flip-onopen')) || + classList.contains('mud-popover-overflow-flip-always')) { + + // we know it's supposed to flip if able + selector = null; + popoverContentNode.removeAttribute('data-mudpopover-flip'); const appBarElements = document.getElementsByClassName("mud-appbar mud-appbar-fixed-top"); let appBarOffset = 0; @@ -243,74 +257,93 @@ window.mudpopoverHelper = { const deltaTop = top - selfRect.height - appBarOffset; const spaceToTop = top - appBarOffset; const deltaBottom = window.innerHeight - top - selfRect.height; - //console.log('self-width: ' + selfRect.width + ' | self-height: ' + selfRect.height); - //console.log('left: ' + deltaToLeft + ' | rigth:' + deltaToRight + ' | top: ' + deltaTop + ' | bottom: ' + deltaBottom + ' | spaceToTop: ' + spaceToTop); - - let selector = popoverContentNode.mudPopoverFliped; if (!selector) { if (classList.contains('mud-popover-top-left')) { - if (deltaBottom < graceMargin && deltaToRight < graceMargin && spaceToTop >= selfRect.height && deltaToLeft >= selfRect.width) { + // get values that determine where the most space is + const shouldFlipVertically = deltaBottom < graceMargin && spaceToTop >= selfRect.height && spaceToTop > deltaBottom; + const shouldFlipHorizontally = deltaToRight < graceMargin && deltaToLeft >= selfRect.width && deltaToLeft > deltaToRight; + + if (shouldFlipVertically && shouldFlipHorizontally) { selector = 'top-and-left'; - } else if (deltaBottom < graceMargin && spaceToTop >= selfRect.height) { + } else if (shouldFlipVertically) { selector = 'top'; - } else if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width) { + } else if (shouldFlipHorizontally) { selector = 'left'; } } else if (classList.contains('mud-popover-top-center')) { - if (deltaBottom < graceMargin && spaceToTop >= selfRect.height) { + if (deltaBottom < graceMargin && spaceToTop >= selfRect.height && spaceToTop > deltaBottom) { selector = 'top'; } } else if (classList.contains('mud-popover-top-right')) { - if (deltaBottom < graceMargin && deltaToLeft < graceMargin && spaceToTop >= selfRect.height && deltaToRight >= selfRect.width) { + const shouldFlipVertically = deltaBottom < graceMargin && spaceToTop >= selfRect.height && spaceToTop > deltaBottom; + const shouldFlipHorizontally = deltaToLeft < graceMargin && deltaToRight >= selfRect.width && deltaToRight > deltaToLeft; + + if (shouldFlipVertically && shouldFlipHorizontally) { selector = 'top-and-right'; - } else if (deltaBottom < graceMargin && spaceToTop >= selfRect.height) { + } else if (shouldFlipVertically) { selector = 'top'; - } else if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width) { + } else if (shouldFlipHorizontally) { selector = 'right'; } } else if (classList.contains('mud-popover-center-left')) { - if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width) { + if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width && deltaToLeft > deltaToRight) { selector = 'left'; } } else if (classList.contains('mud-popover-center-right')) { - if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width) { + if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width && deltaToRight > deltaToLeft) { selector = 'right'; } } else if (classList.contains('mud-popover-bottom-left')) { - if (deltaTop < graceMargin && deltaToRight < graceMargin && deltaBottom >= 0 && deltaToLeft >= selfRect.width) { + const shouldFlipVertically = deltaTop < graceMargin && deltaBottom >= 0 && deltaBottom > deltaTop; + const shouldFlipHorizontally = deltaToRight < graceMargin && deltaToLeft >= selfRect.width && deltaToLeft > deltaToRight; + + if (shouldFlipVertically && shouldFlipHorizontally) { selector = 'bottom-and-left'; - } else if (deltaTop < graceMargin && deltaBottom >= 0) { + } else if (shouldFlipVertically) { selector = 'bottom'; - } else if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width) { + } else if (shouldFlipHorizontally) { selector = 'left'; } } else if (classList.contains('mud-popover-bottom-center')) { - if (deltaTop < graceMargin && deltaBottom >= 0) { + if (deltaTop < graceMargin && deltaBottom >= 0 && deltaBottom > deltaTop) { selector = 'bottom'; } } else if (classList.contains('mud-popover-bottom-right')) { - if (deltaTop < graceMargin && deltaToLeft < graceMargin && deltaBottom >= 0 && deltaToRight >= selfRect.width) { + const shouldFlipVertically = deltaTop < graceMargin && deltaBottom >= 0 && deltaBottom > deltaTop; + const shouldFlipHorizontally = deltaToLeft < graceMargin && deltaToRight >= selfRect.width && deltaToRight > deltaToLeft; + + if (shouldFlipVertically && shouldFlipHorizontally) { selector = 'bottom-and-right'; - } else if (deltaTop < graceMargin && deltaBottom >= 0) { + } else if (shouldFlipVertically) { selector = 'bottom'; - } else if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width) { + } else if (shouldFlipHorizontally) { selector = 'right'; } } } + + // selector is set in above if statement if it needs to flip if (selector && selector != 'none') { const newPosition = window.mudpopoverHelper.getPositionForFlippedPopver(classListArray, selector, boundingRect, selfRect); left = newPosition.left; top = newPosition.top; offsetX = newPosition.offsetX; offsetY = newPosition.offsetY; + //console.log(`what now: ${selector} | left: ${left} | top: ${top} | offsetX: ${offsetX} | offsetY: ${offsetY}`); popoverContentNode.setAttribute('data-mudpopover-flip', 'flipped'); + + // if we are flip on open then store the true top/left + if (classList.contains('mud-popover-overflow-flip-onopen')) { + console.log(`setting flip top: ${top} | left: ${ left }`); + popoverContentNode.setAttribute('data-flip-left', left); + popoverContentNode.setAttribute('data-flip-top', top); + } } else { // did not flip, ensure the left and top are inside bounds @@ -344,13 +377,6 @@ window.mudpopoverHelper = { if (list && list.offsetHeight > listMaxHeight) { list.style.maxHeight = (listMaxHeight - listPadding) + 'px'; } - popoverContentNode.removeAttribute('data-mudpopover-flip'); - } - - if (classList.contains('mud-popover-overflow-flip-onopen')) { - if (!popoverContentNode.mudPopoverFliped) { - popoverContentNode.mudPopoverFliped = selector || 'none'; - } } } @@ -368,6 +394,14 @@ window.mudpopoverHelper = { offsetY = 0; } + // if it's been set to flip-onopen get the top/left from the attributes to reapply the offset + if (selector === 'flipped' && classList.contains('mud-popover-overflow-flip-onopen')) { + console.log(`calculated left ${left} | top ${top}`); + left = parseInt(popoverContentNode.getAttribute('data-flip-left')) || left; + top = parseInt(popoverContentNode.getAttribute('data-flip-top')) || top; + console.log(`left: ${left} | top: ${top} | offsetX: ${offsetX} | offsetY: ${offsetY}`); + } + popoverContentNode.style['left'] = (left + offsetX) + 'px'; popoverContentNode.style['top'] = (top + offsetY) + 'px'; @@ -386,25 +420,28 @@ window.mudpopoverHelper = { } }, + // cycles through popovers to reposition those that are open, classSelector is passed on placePopoverByClassSelector: function (classSelector = null) { var items = window.mudPopover.getAllObservedContainers(); - for (let i = 0; i < items.length; i++) { const popoverNode = document.getElementById('popover-' + items[i]); window.mudpopoverHelper.placePopover(popoverNode, classSelector); } }, + // used in the initial placement of a popover placePopoverByNode: function (target) { const id = target.id.substr(15); const popoverNode = document.getElementById('popover-' + id); window.mudpopoverHelper.placePopover(popoverNode); }, + // returns the count of providers countProviders: function () { return document.querySelectorAll(`.${window.mudpopoverHelper.mainContainerClass}`).length; }, + // sets popoveroverlay to the right z-index updatePopoverOverlay: function (popoverContentNode) { // tooltips don't have an overlay if (popoverContentNode.classList.contains("mud-tooltip")) { @@ -425,6 +462,7 @@ window.mudpopoverHelper = { } }, + // set zindex order updatePopoverZIndex: function (popoverContentNode, parentNode) { // find the first parent mud-popover if it exists const parentPopover = parentNode.closest('.mud-popover'); @@ -480,6 +518,27 @@ window.mudpopoverHelper = { popoverContentNode.style['z-index'] = newZIndex; } }, + + // adds scroll listeners to node + parents up to body + popoverScrollListener: function (node) { + let currentNode = node.parentNode; + const scrollableElements = []; + while (currentNode) { + const isScrollable = + (currentNode.scrollHeight > currentNode.clientHeight) || // Vertical scroll + (currentNode.scrollWidth > currentNode.clientWidth); // Horizontal scroll + if (isScrollable) { + currentNode.addEventListener('scroll', handleScroll, { passive: true }); + scrollableElements.push(currentNode); + } + // Stop if we reach the body, or head + if (currentNode.tagName === "BODY") { + break; + } + currentNode = currentNode.parentNode; + } + return scrollableElements; + }, } class MudPopover { @@ -489,88 +548,60 @@ class MudPopover { this.contentObserver = null; } - callback(id, mutationsList, observer) { - for (const mutation of mutationsList) { - if (mutation.type === 'attributes') { - const target = mutation.target - if (mutation.attributeName == 'class') { - if (target.classList.contains('mud-popover-overflow-flip-onopen') && - target.classList.contains('mud-popover-open') == false) { - target.mudPopoverFliped = null; - target.removeAttribute('data-mudpopover-flip'); - } - - window.mudpopoverHelper.placePopoverByNode(target); + callbackPopover(mutation) { + const target = mutation.target; + if (!target) return; + // we use top and left negative numbers to prevent showing until done with this method + if (mutation.type == 'attributes' && mutation.attributeName == 'data-ticks') { + // when data-ticks attribute is the mutation something has changed with the popover + // and it needs to be repositioned and shown, note we don't use mud-popover-open here + // instead we use data-ticks since we know the newest data-ticks > 0 is the top most. + const tickAttribute = target.getAttribute('data-ticks'); + // if data-ticks is 0 the popover isn't open and it's hidden in css but we don't want it to reappear until + // it's positioned the next time so we move it off screen + if (tickAttribute == 0) { + // wait this long until we "move it off screen" + const delay = parseFloat(target.style['transition-duration']) || 0; + if (delay == 0) { + target.style['left'] = '-9999px'; + target.style['top'] = '-9999px'; } - else if (mutation.attributeName == 'data-ticks') { - // data-ticks are important for Direction and Location, it doesn't reposition - // if they aren't there - const tickAttribute = target.getAttribute('data-ticks'); - - const tickValues = []; - let max = -1; - if (parent && parent.children) { - for (let i = 0; i < parent.children.length; i++) { - const childNode = parent.children[i]; - const tickValue = parseInt(childNode.getAttribute('data-ticks')); - if (tickValue == 0) { - continue; - } - - if (tickValues.indexOf(tickValue) >= 0) { - continue; - } - - tickValues.push(tickValue); - - if (tickValue > max) { - max = tickValue; - } - } - } + setTimeout(() => { + target.style['left'] = '-9999px'; + target.style['top'] = '-9999px'; + }, delay); - // Iterate over the items in this.map to reset any open overlays - let highestTickItem = null; - let highestTickValue = -1; - // Iterate over the items in this.map to find the highest data-ticks value - for (const mapItem of Object.values(this.map)) { - const popoverContentNode = mapItem.popoverContentNode; - if (popoverContentNode) { - const tickValue = Number(popoverContentNode.getAttribute('data-ticks')); // Convert to Number - - if (tickValue > highestTickValue) { - highestTickValue = tickValue; - highestTickItem = popoverContentNode; - } - } - } + // reset flip status + target.removeAttribute('data-mudpopover-flip'); + } + // data ticks is not 0 so let's reposition the popover and overlay + else if (target.parentNode && + target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { - if (highestTickItem) { - window.mudpopoverHelper.updatePopoverOverlay(highestTickItem); - } + // reposition popover + window.mudpopoverHelper.placePopoverByNode(target); - if (tickValues.length == 0) { - continue; - } + // check and reposition overlay if needed + let highestTickItem = null; + let highestTickValue = -1; - const sortedTickValues = tickValues.sort((x, y) => x - y); - // z-index calculation not used here - continue; - for (let i = 0; i < parent.children.length; i++) { - const childNode = parent.children[i]; - const tickValue = parseInt(childNode.getAttribute('data-ticks')); - if (tickValue == 0) { - continue; - } + // Traverse children of target.parentNode that contain the class "mud-popover" + for (const child of target.parentNode.children) { + if (child.classList.contains("mud-popover")) { + const tickValue = Number(child.getAttribute("data-ticks")) || 0; - if (childNode.skipZIndex == true) { - continue; + if (tickValue > highestTickValue) { + highestTickValue = tickValue; + highestTickItem = child; } - const newIndex = window.mudpopoverHelper.basePopoverZIndex + sortedTickValues.indexOf(tickValue) + 3; - childNode.style['z-index'] = newIndex; } } + + if (highestTickItem) { + window.mudpopoverHelper.updatePopoverOverlay(highestTickItem); + } } + } } @@ -594,16 +625,16 @@ class MudPopover { const config = { attributes: true, // only observe attributes subtree: true, // all descendants of popover - attributeFilter: ['class', 'data-ticks'] + attributeFilter: ['data-ticks'] // limit to just data-ticks }; const observer = new MutationObserver((mutations) => { - for (const mutation in mutations) { + for (const mutation of mutations) { // if it's direct parent is the provider // and contains the class mud-popover if (mutation.target.parentNode === provider && mutation.target.classList.contains('mud-popover')) { - console.log('Change detected on popover element:', mutation.target.id); - this.callback.bind(this, mutation.target.id) + // console.log('Change detected on popover element:', mutation.target.id); + this.callbackPopover(mutation); } } }); @@ -627,62 +658,51 @@ class MudPopover { if (popoverNode && popoverNode.parentNode && popoverContentNode) { - window.mudpopoverHelper.placePopover(popoverNode); - - const config = { attributeFilter: ['class', 'data-ticks'] }; - - const observer = new MutationObserver(this.callback.bind(this, id)); - - // observer.observe(popoverContentNode, config); + // Add scroll event listeners to the content node and its parents up to the Body + const scrollableElements = window.mudpopoverHelper.popoverScrollListener(popoverNode); + // add a resize observor to catch resize events const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const target = entry.target; for (const childNode of target.childNodes) { if (childNode.id && childNode.id.startsWith('popover-')) { - window.mudpopoverHelper.placePopover(childNode); + debouncedResize(); } } } }); - // resizeObserver.observe(popoverNode.parentNode); - - const contentNodeObserver = new ResizeObserver(entries => { - for (let entry of entries) { - const target = entry.target; - if (target) - window.mudpopoverHelper.placePopoverByNode(target); - } - }); - - // contentNodeObserver.observe(popoverContentNode); + resizeObserver.observe(popoverNode.parentNode); this.map[id] = { popoverContentNode: popoverContentNode, - mutationObserver: observer, - resizeObserver: resizeObserver, - contentNodeObserver: contentNodeObserver + scrollableElements: scrollableElements, + parentResizeObserver: resizeObserver, }; } } disconnect(id) { if (this.map[id]) { + // Remove scroll event listeners from the stored scrollable elements + const { scrollableElements } = this.map[id]; + + scrollableElements.forEach(element => { + element.removeEventListener('scroll', handleScroll); + }); - const item = this.map[id] - item.mutationObserver.disconnect(); - item.resizeObserver.disconnect(); - item.contentNodeObserver.disconnect(); + // Remove resize observer + this.map[id].parentResizeObserver.disconnect(); delete this.map[id]; } } dispose() { - // for (var i in this.map) { - // disconnect(i); - // } + for (var i in this.map) { + this.disconnect(i); + } this.contentObserver.disconnect(); this.contentObserver = null; @@ -702,12 +722,12 @@ window.mudPopover = new MudPopover(); const debouncedResize = window.mudpopoverHelper.debounce(() => { window.mudpopoverHelper.placePopoverByClassSelector(); -}, 100); +}, 25); -const throttledScroll = window.mudpopoverHelper.rafThrottle(() => { +const handleScroll = function () { window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); -}); +}; window.addEventListener('resize', debouncedResize, { passive: true }); -window.addEventListener('scroll', throttledScroll, { passive: true }); \ No newline at end of file +window.addEventListener('scroll', handleScroll, { passive: true }); \ No newline at end of file From 7dc0d1d7b1b030a68ec93d02f537116616e6776e Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Fri, 4 Apr 2025 18:12:56 -0500 Subject: [PATCH 04/22] flipping logic check complete --- src/MudBlazor/TScripts/mudPopover.js | 51 ++++++++++------------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index a75c70e52a39..abdb42957c52 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -28,7 +28,7 @@ window.mudpopoverHelper = { baseTooltipZIndex: parseInt(getComputedStyle(document.documentElement) .getPropertyValue('--mud-zindex-tooltip')) || 1600, - // static set of replacement valus + // static set of replacement values flipClassReplacements: { 'top': { 'mud-popover-top-left': 'mud-popover-bottom-left', @@ -140,7 +140,7 @@ window.mudpopoverHelper = { offsetY = -selfRect.height; } - console.log(`top: ${top}, left: ${left}, offsetX: ${offsetX}, offsetY: ${offsetY}`); + // console.log(`top: ${top}, left: ${left}, offsetX: ${offsetX}, offsetY: ${offsetY}`); return { top: top, left: left, offsetX: offsetX, offsetY: offsetY }; @@ -162,7 +162,7 @@ window.mudpopoverHelper = { classList.push(item); } } - console.log(replacementsList); + // console.log(replacementsList); return window.mudpopoverHelper.calculatePopoverPosition(classList, boundingRect, selfRect); }, @@ -179,7 +179,6 @@ window.mudpopoverHelper = { if (!popoverContentNode) { return; } - const classList = popoverContentNode.classList; // if the popover isn't open we stop @@ -213,7 +212,6 @@ window.mudpopoverHelper = { let top = postion.top; let offsetX = postion.offsetX; let offsetY = postion.offsetY; - // get the top/left/ from popoverContentNode if the popover has been hardcoded for position if (classList.contains('mud-popover-position-override')) { left = parseInt(popoverContentNode.style['left']) || left; @@ -230,20 +228,9 @@ window.mudpopoverHelper = { width: selfRect.width, height: selfRect.height }; - } - + } // flipping logic - // get flip status - let selector = popoverContentNode.getAttribute("data-mudpopover-flip"); - console.log(`starting: ${selector}`); - // if there has not been a flip and it's got an onopen class OR - // there is a flip-always class - if ((!selector && classList.contains('mud-popover-overflow-flip-onopen')) || - classList.contains('mud-popover-overflow-flip-always')) { - - // we know it's supposed to flip if able - selector = null; - popoverContentNode.removeAttribute('data-mudpopover-flip'); + if (classList.contains('mud-popover-overflow-flip-onopen') || classList.contains('mud-popover-overflow-flip-always')) { const appBarElements = document.getElementsByClassName("mud-appbar mud-appbar-fixed-top"); let appBarOffset = 0; @@ -257,6 +244,10 @@ window.mudpopoverHelper = { const deltaTop = top - selfRect.height - appBarOffset; const spaceToTop = top - appBarOffset; const deltaBottom = window.innerHeight - top - selfRect.height; + //console.log('self-width: ' + selfRect.width + ' | self-height: ' + selfRect.height); + //console.log('left: ' + deltaToLeft + ' | rigth:' + deltaToRight + ' | top: ' + deltaTop + ' | bottom: ' + deltaBottom + ' | spaceToTop: ' + spaceToTop); + + let selector = popoverContentNode.mudPopoverFliped; if (!selector) { if (classList.contains('mud-popover-top-left')) { @@ -335,15 +326,7 @@ window.mudpopoverHelper = { top = newPosition.top; offsetX = newPosition.offsetX; offsetY = newPosition.offsetY; - //console.log(`what now: ${selector} | left: ${left} | top: ${top} | offsetX: ${offsetX} | offsetY: ${offsetY}`); popoverContentNode.setAttribute('data-mudpopover-flip', 'flipped'); - - // if we are flip on open then store the true top/left - if (classList.contains('mud-popover-overflow-flip-onopen')) { - console.log(`setting flip top: ${top} | left: ${ left }`); - popoverContentNode.setAttribute('data-flip-left', left); - popoverContentNode.setAttribute('data-flip-top', top); - } } else { // did not flip, ensure the left and top are inside bounds @@ -377,6 +360,13 @@ window.mudpopoverHelper = { if (list && list.offsetHeight > listMaxHeight) { list.style.maxHeight = (listMaxHeight - listPadding) + 'px'; } + popoverContentNode.removeAttribute('data-mudpopover-flip'); + } + + if (classList.contains('mud-popover-overflow-flip-onopen')) { + if (!popoverContentNode.mudPopoverFliped) { + popoverContentNode.mudPopoverFliped = selector || 'none'; + } } } @@ -394,14 +384,6 @@ window.mudpopoverHelper = { offsetY = 0; } - // if it's been set to flip-onopen get the top/left from the attributes to reapply the offset - if (selector === 'flipped' && classList.contains('mud-popover-overflow-flip-onopen')) { - console.log(`calculated left ${left} | top ${top}`); - left = parseInt(popoverContentNode.getAttribute('data-flip-left')) || left; - top = parseInt(popoverContentNode.getAttribute('data-flip-top')) || top; - console.log(`left: ${left} | top: ${top} | offsetX: ${offsetX} | offsetY: ${offsetY}`); - } - popoverContentNode.style['left'] = (left + offsetX) + 'px'; popoverContentNode.style['top'] = (top + offsetY) + 'px'; @@ -572,6 +554,7 @@ class MudPopover { }, delay); // reset flip status + target.mudPopoverFliped = null; target.removeAttribute('data-mudpopover-flip'); } // data ticks is not 0 so let's reposition the popover and overlay From 17ccef9516b9a7c1997a5ef1d455350d67ae3dab Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Sat, 5 Apr 2025 15:20:48 -0500 Subject: [PATCH 05/22] Improve readability and maintainability of mudPopover.js Refactor variable names and enhance comments for clarity. Correct spelling of 'postion' to 'position'. Simplify conditional checks for popover flipping logic. Use removeProperty for off-screen positioning instead of setting styles to '-9999px'. These changes aim to maintain functionality while making the code more understandable and easier to maintain. --- src/MudBlazor/TScripts/mudPopover.js | 111 +++++++++++++-------------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index abdb42957c52..8d552291898c 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -207,11 +207,11 @@ window.mudpopoverHelper = { const classListArray = Array.from(classList); // calculate position based on opening anchor/transform - const postion = window.mudpopoverHelper.calculatePopoverPosition(classListArray, boundingRect, selfRect); - let left = postion.left; - let top = postion.top; - let offsetX = postion.offsetX; - let offsetY = postion.offsetY; + const position = window.mudpopoverHelper.calculatePopoverPosition(classListArray, boundingRect, selfRect); + let left = position.left; // X-coordinate of the popover + let top = position.top; // Y-coordinate of the popover + let offsetX = position.offsetX; // Horizontal offset of the popover + let offsetY = position.offsetY; // Vertical offset of the popover // get the top/left/ from popoverContentNode if the popover has been hardcoded for position if (classList.contains('mud-popover-position-override')) { left = parseInt(popoverContentNode.style['left']) || left; @@ -238,87 +238,77 @@ window.mudpopoverHelper = { appBarOffset = appBarElements[0].getBoundingClientRect().height; } - const graceMargin = window.mudpopoverHelper.flipMargin; - const deltaToLeft = left + offsetX; - const deltaToRight = window.innerWidth - left - selfRect.width; - const deltaTop = top - selfRect.height - appBarOffset; - const spaceToTop = top - appBarOffset; - const deltaBottom = window.innerHeight - top - selfRect.height; - //console.log('self-width: ' + selfRect.width + ' | self-height: ' + selfRect.height); - //console.log('left: ' + deltaToLeft + ' | rigth:' + deltaToRight + ' | top: ' + deltaTop + ' | bottom: ' + deltaBottom + ' | spaceToTop: ' + spaceToTop); + const graceMargin = window.mudpopoverHelper.flipMargin; // Margin for flipping logic + const deltaToLeft = left + offsetX; // Distance to the left edge of the screen + const deltaToRight = window.innerWidth - left - selfRect.width; // Distance to the right edge of the screen + const deltaTop = top - selfRect.height - appBarOffset; // Distance to the top edge of the screen + const spaceToTop = top - appBarOffset; // Space available above the popover + const deltaBottom = window.innerHeight - top - selfRect.height; // Distance to the bottom edge of the screen + // mudPopoverFliped is the flip direction for first flip on flip - onopen popovers let selector = popoverContentNode.mudPopoverFliped; + // flip routine off transform origin, sets selector to an axis to flip on if needed if (!selector) { - if (classList.contains('mud-popover-top-left')) { - // get values that determine where the most space is - const shouldFlipVertically = deltaBottom < graceMargin && spaceToTop >= selfRect.height && spaceToTop > deltaBottom; - const shouldFlipHorizontally = deltaToRight < graceMargin && deltaToLeft >= selfRect.width && deltaToLeft > deltaToRight; + // ignoring popover see which sides have more space, we already have top/left as set by anchor origin + const right = window.innerWidth - left; + const bottom = window.innerHeight - top; - if (shouldFlipVertically && shouldFlipHorizontally) { + if (classList.contains('mud-popover-top-left')) { + if (deltaBottom < graceMargin && deltaToRight < graceMargin && spaceToTop >= selfRect.height && deltaToLeft >= selfRect.width + || (selfRect.height > top && selfRect.width > left && bottom > top && right > left)) { selector = 'top-and-left'; - } else if (shouldFlipVertically) { + } else if ((deltaBottom < graceMargin && spaceToTop >= selfRect.height) || (selfRect.height > top && bottom > top)) { selector = 'top'; - } else if (shouldFlipHorizontally) { + } else if ((deltaToRight < graceMargin && deltaToLeft >= selfRect.width) || (selfRect.width > left && right > left)) { selector = 'left'; } } else if (classList.contains('mud-popover-top-center')) { - if (deltaBottom < graceMargin && spaceToTop >= selfRect.height && spaceToTop > deltaBottom) { + if ((deltaBottom < graceMargin && spaceToTop >= selfRect.height) || (selfRect.height > top && bottom > top)) { selector = 'top'; } } else if (classList.contains('mud-popover-top-right')) { - const shouldFlipVertically = deltaBottom < graceMargin && spaceToTop >= selfRect.height && spaceToTop > deltaBottom; - const shouldFlipHorizontally = deltaToLeft < graceMargin && deltaToRight >= selfRect.width && deltaToRight > deltaToLeft; - - if (shouldFlipVertically && shouldFlipHorizontally) { + if ((deltaBottom < graceMargin && deltaToLeft < graceMargin && spaceToTop >= selfRect.height && deltaToRight >= selfRect.width) + || (selfRect.height > top && selfRect.width > left && bottom > top && right > left)) { selector = 'top-and-right'; - } else if (shouldFlipVertically) { + } else if ((deltaBottom < graceMargin && spaceToTop >= selfRect.height) || (selfRect.height > top && bottom > top)) { selector = 'top'; - } else if (shouldFlipHorizontally) { + } else if ((deltaToLeft < graceMargin && deltaToRight >= selfRect.width) || (selfRect.width > left && right > left)) { selector = 'right'; } - } - - else if (classList.contains('mud-popover-center-left')) { - if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width && deltaToLeft > deltaToRight) { + } else if (classList.contains('mud-popover-center-left')) { + if ((deltaToRight < graceMargin && deltaToLeft >= selfRect.width) || (selfRect.width > left && right > left)) { selector = 'left'; } - } - else if (classList.contains('mud-popover-center-right')) { - if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width && deltaToRight > deltaToLeft) { + } else if (classList.contains('mud-popover-center-right')) { + if ((deltaToLeft < graceMargin && deltaToRight >= selfRect.width) || (selfRect.width > right && left > right)) { selector = 'right'; } - } - else if (classList.contains('mud-popover-bottom-left')) { - const shouldFlipVertically = deltaTop < graceMargin && deltaBottom >= 0 && deltaBottom > deltaTop; - const shouldFlipHorizontally = deltaToRight < graceMargin && deltaToLeft >= selfRect.width && deltaToLeft > deltaToRight; - - if (shouldFlipVertically && shouldFlipHorizontally) { + } else if (classList.contains('mud-popover-bottom-left')) { + if ((deltaTop < graceMargin && deltaToRight < graceMargin && deltaBottom >= 0 && deltaToLeft >= selfRect.width) + || (selfRect.height > bottom && selfRect.width > left && top > bottom && right > left)) { selector = 'bottom-and-left'; - } else if (shouldFlipVertically) { + } else if ((deltaTop < graceMargin && deltaBottom >= 0) || (selfRect.height > bottom && top > bottom)) { selector = 'bottom'; - } else if (shouldFlipHorizontally) { + } else if ((deltaToRight < graceMargin && deltaToLeft >= selfRect.width) || (selfRect.width > left && right > left)) { selector = 'left'; } } else if (classList.contains('mud-popover-bottom-center')) { - if (deltaTop < graceMargin && deltaBottom >= 0 && deltaBottom > deltaTop) { + if ((deltaTop < graceMargin && deltaBottom >= 0) || (selfRect.height > bottom && top > bottom)) { selector = 'bottom'; } } else if (classList.contains('mud-popover-bottom-right')) { - const shouldFlipVertically = deltaTop < graceMargin && deltaBottom >= 0 && deltaBottom > deltaTop; - const shouldFlipHorizontally = deltaToLeft < graceMargin && deltaToRight >= selfRect.width && deltaToRight > deltaToLeft; - - if (shouldFlipVertically && shouldFlipHorizontally) { + if ((deltaTop < graceMargin && deltaToLeft < graceMargin && deltaBottom >= 0 && deltaToRight >= selfRect.width) + || (selfRect.height > bottom && selfRect.width > right && top > bottom && left > right)) { selector = 'bottom-and-right'; - } else if (shouldFlipVertically) { + } else if ((deltaTop < graceMargin && deltaBottom >= 0) || (selfRect.height > bottom && top > bottom)) { selector = 'bottom'; - } else if (shouldFlipHorizontally) { + } else if ((deltaToLeft < graceMargin && deltaToRight >= selfRect.width) || (selfRect.width > right && left > right)) { selector = 'right'; } } } - // selector is set in above if statement if it needs to flip if (selector && selector != 'none') { const newPosition = window.mudpopoverHelper.getPositionForFlippedPopver(classListArray, selector, boundingRect, selfRect); @@ -545,25 +535,31 @@ class MudPopover { // wait this long until we "move it off screen" const delay = parseFloat(target.style['transition-duration']) || 0; if (delay == 0) { - target.style['left'] = '-9999px'; - target.style['top'] = '-9999px'; + // remove left and top styles + target.style.removeProperty('left'); + target.style.removeProperty('top'); } setTimeout(() => { - target.style['left'] = '-9999px'; - target.style['top'] = '-9999px'; + target.style.removeProperty('left'); + target.style.removeProperty('top'); }, delay); // reset flip status target.mudPopoverFliped = null; target.removeAttribute('data-mudpopover-flip'); + + // tell the map that this popover is closed + const id = target.id.substr(15); + this.map[id].isOpened = false; } // data ticks is not 0 so let's reposition the popover and overlay else if (target.parentNode && target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { - + console.log(mutation); + const currTick = Number(target.getAttribute("data-ticks")) || 0; + console.log(`data-ticks: ${currTick} for ${target.id}`); // reposition popover window.mudpopoverHelper.placePopoverByNode(target); - // check and reposition overlay if needed let highestTickItem = null; let highestTickValue = -1; @@ -616,7 +612,6 @@ class MudPopover { // if it's direct parent is the provider // and contains the class mud-popover if (mutation.target.parentNode === provider && mutation.target.classList.contains('mud-popover')) { - // console.log('Change detected on popover element:', mutation.target.id); this.callbackPopover(mutation); } } @@ -704,10 +699,12 @@ class MudPopover { window.mudPopover = new MudPopover(); const debouncedResize = window.mudpopoverHelper.debounce(() => { + console.log("resize"); window.mudpopoverHelper.placePopoverByClassSelector(); }, 25); const handleScroll = function () { + console.log("scroll"); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); }; From 4a2ccde3b9724855f0e69cc768f7c483b609c01f Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Sat, 5 Apr 2025 15:21:10 -0500 Subject: [PATCH 06/22] test viewer cases --- .../Popover/PopoverFlipDirectionTest.razor | 118 ++++++++++++++++++ .../Tooltip/TooltipNotRemovedTest.razor | 33 +++++ 2 files changed, 151 insertions(+) create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor new file mode 100644 index 000000000000..27799fcdec79 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor @@ -0,0 +1,118 @@ + + + @(_expanded ? "Collapse" : "Expand") + @(_expanded ? "Collapse" : "Expand") for 5 s + Expand list in 5 s + + +
+ + + + Top Left + Top Center + Top Right + Bottom Left + Bottom Center + Bottom Right + Center Left + Center Center + Center Right + + + Top Left + Top Center + Top Right + Bottom Left + Bottom Center + Bottom Right + Center Left + Center Center + Center Right + +
+
+ + + + + + + + + + @if (_expandList) + { + + + + + + + + + + + + + + + + + + + } + + @if (!_hideBottom) + { + @(_expanded ? "Collapse" : "Expand") + @(_expanded ? "Collapse" : "Expand") for 5 s + + +
+ +
+
+ } +
+@code { + public static string __description__ = "Popover should take the best available space"; + + private Int32 _height { get; set; } = 600; + private Int32 _heightBottom { get; set; } = 400; + private Boolean _hideBottom { get; set; } + private Boolean _expandList { get; set; } + private Origin _anchor = Origin.BottomLeft; + private Origin _transform = Origin.TopLeft; + private bool _expanded = true; + + private DropdownSettings _dropdownSettings => new DropdownSettings + { + OverflowBehavior = OverflowBehavior.FlipAlways + }; + + private void OnExpandCollapseClick() + { + _expanded = !_expanded; + } + + private async Task OnExpandCollapseFor5sClick() + { + var cur = _expanded; + _expanded = !cur; + await Task.Delay(5000); + _expanded = cur; + } + + private async Task OnExpandListIn5s() + { + _expandList = false; + await Task.Delay(5000); + _expandList = true; + } +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor new file mode 100644 index 000000000000..2c39811d9cce --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor @@ -0,0 +1,33 @@ + + + @for (var i = 0; i < 200; i++) + { + +
+ + BTN @i + +
+ } +
+ +@code +{ + private Guid key; + + protected override Task OnInitializedAsync() + { + RunThing().CatchAndLog(); + return base.OnInitializedAsync(); + } + + private async Task RunThing() + { + while (true) + { + this.key = Guid.NewGuid(); + InvokeAsync(StateHasChanged).CatchAndLog(); + await Task.Delay(100); + } + } +} From ab2fac01181ffd848c53ed939b004bddf2e5c207 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Sun, 6 Apr 2025 13:01:16 -0500 Subject: [PATCH 07/22] updated changes, still working on anchorpoint and top section --- src/MudBlazor/TScripts/mudPopover.js | 228 +++++++++++++++++++-------- 1 file changed, 162 insertions(+), 66 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 8d552291898c..b6e927e88f94 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -140,7 +140,6 @@ window.mudpopoverHelper = { offsetY = -selfRect.height; } - // console.log(`top: ${top}, left: ${left}, offsetX: ${offsetX}, offsetY: ${offsetY}`); return { top: top, left: left, offsetX: offsetX, offsetY: offsetY }; @@ -162,7 +161,6 @@ window.mudpopoverHelper = { classList.push(item); } } - // console.log(replacementsList); return window.mudpopoverHelper.calculatePopoverPosition(classList, boundingRect, selfRect); }, @@ -238,75 +236,159 @@ window.mudpopoverHelper = { appBarOffset = appBarElements[0].getBoundingClientRect().height; } - const graceMargin = window.mudpopoverHelper.flipMargin; // Margin for flipping logic - const deltaToLeft = left + offsetX; // Distance to the left edge of the screen - const deltaToRight = window.innerWidth - left - selfRect.width; // Distance to the right edge of the screen - const deltaTop = top - selfRect.height - appBarOffset; // Distance to the top edge of the screen const spaceToTop = top - appBarOffset; // Space available above the popover - const deltaBottom = window.innerHeight - top - selfRect.height; // Distance to the bottom edge of the screen - // mudPopoverFliped is the flip direction for first flip on flip - onopen popovers let selector = popoverContentNode.mudPopoverFliped; // flip routine off transform origin, sets selector to an axis to flip on if needed if (!selector) { - // ignoring popover see which sides have more space, we already have top/left as set by anchor origin - const right = window.innerWidth - left; - const bottom = window.innerHeight - top; + // For mud-popover-top-left if (classList.contains('mud-popover-top-left')) { - if (deltaBottom < graceMargin && deltaToRight < graceMargin && spaceToTop >= selfRect.height && deltaToLeft >= selfRect.width - || (selfRect.height > top && selfRect.width > left && bottom > top && right > left)) { + // Space available in current direction + const spaceBelow = window.innerHeight - top; // Space below the anchor + const spaceRight = window.innerWidth - left; // Space to the right of the anchor + + // Space available in opposite direction + const spaceAbove = spaceToTop; + const spaceLeft = left; + + // Check if popover exceeds available space AND if opposite side has more space + const shouldFlipVertical = selfRect.height > spaceBelow && spaceAbove > spaceBelow; + const shouldFlipHorizontal = selfRect.width > spaceRight && spaceLeft > spaceRight; + + // Apply flips based on space comparisons + if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'top-and-left'; - } else if ((deltaBottom < graceMargin && spaceToTop >= selfRect.height) || (selfRect.height > top && bottom > top)) { + } + else if (shouldFlipVertical) { selector = 'top'; - } else if ((deltaToRight < graceMargin && deltaToLeft >= selfRect.width) || (selfRect.width > left && right > left)) { + } + else if (shouldFlipHorizontal) { selector = 'left'; } - } else if (classList.contains('mud-popover-top-center')) { - if ((deltaBottom < graceMargin && spaceToTop >= selfRect.height) || (selfRect.height > top && bottom > top)) { + } + + // For mud-popover-top-center + else if (classList.contains('mud-popover-top-center')) { + // Space available in current direction vs opposite direction + const spaceBelow = window.innerHeight - top; + const spaceAbove = spaceToTop; + + // Only flip if popover exceeds available space AND there's more space in opposite direction + if (selfRect.height > spaceBelow && spaceAbove > spaceBelow) { selector = 'top'; } - } else if (classList.contains('mud-popover-top-right')) { - if ((deltaBottom < graceMargin && deltaToLeft < graceMargin && spaceToTop >= selfRect.height && deltaToRight >= selfRect.width) - || (selfRect.height > top && selfRect.width > left && bottom > top && right > left)) { + } + + // For mud-popover-top-right + else if (classList.contains('mud-popover-top-right')) { + // Space available in current direction + const spaceBelow = window.innerHeight - top; + const spaceLeft = left; + + // Space available in opposite direction + const spaceAbove = spaceToTop; + const spaceRight = window.innerWidth - left; + + // Check if popover exceeds available space AND if opposite side has more space + const shouldFlipVertical = selfRect.height > spaceBelow && spaceAbove > spaceBelow; + const shouldFlipHorizontal = selfRect.width > spaceLeft && spaceRight > spaceLeft; + + if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'top-and-right'; - } else if ((deltaBottom < graceMargin && spaceToTop >= selfRect.height) || (selfRect.height > top && bottom > top)) { + } + else if (shouldFlipVertical) { selector = 'top'; - } else if ((deltaToLeft < graceMargin && deltaToRight >= selfRect.width) || (selfRect.width > left && right > left)) { + } + else if (shouldFlipHorizontal) { selector = 'right'; } - } else if (classList.contains('mud-popover-center-left')) { - if ((deltaToRight < graceMargin && deltaToLeft >= selfRect.width) || (selfRect.width > left && right > left)) { + } + + // For mud-popover-center-left + else if (classList.contains('mud-popover-center-left')) { + // Space available in current vs opposite direction + const spaceRight = window.innerWidth - left; + const spaceLeft = left; + + if (selfRect.width > spaceRight && spaceLeft > spaceRight) { selector = 'left'; } - } else if (classList.contains('mud-popover-center-right')) { - if ((deltaToLeft < graceMargin && deltaToRight >= selfRect.width) || (selfRect.width > right && left > right)) { + } + + // For mud-popover-center-right + else if (classList.contains('mud-popover-center-right')) { + // Space available in current vs opposite direction + const spaceLeft = left; + const spaceRight = window.innerWidth - left; + + if (selfRect.width > spaceLeft && spaceRight > spaceLeft) { selector = 'right'; } - } else if (classList.contains('mud-popover-bottom-left')) { - if ((deltaTop < graceMargin && deltaToRight < graceMargin && deltaBottom >= 0 && deltaToLeft >= selfRect.width) - || (selfRect.height > bottom && selfRect.width > left && top > bottom && right > left)) { + } + + // For mud-popover-bottom-left + else if (classList.contains('mud-popover-bottom-left')) { + // Space available in current direction + const spaceAbove = top; + const spaceRight = window.innerWidth - left; + + // Space available in opposite direction + const spaceBelow = window.innerHeight - top; + const spaceLeft = left; + + // Check if popover exceeds available space AND if opposite side has more space + const shouldFlipVertical = selfRect.height > spaceAbove && spaceBelow > spaceAbove; + const shouldFlipHorizontal = selfRect.width > spaceRight && spaceLeft > spaceRight; + + if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'bottom-and-left'; - } else if ((deltaTop < graceMargin && deltaBottom >= 0) || (selfRect.height > bottom && top > bottom)) { + } + else if (shouldFlipVertical) { selector = 'bottom'; - } else if ((deltaToRight < graceMargin && deltaToLeft >= selfRect.width) || (selfRect.width > left && right > left)) { + } + else if (shouldFlipHorizontal) { selector = 'left'; } - } else if (classList.contains('mud-popover-bottom-center')) { - if ((deltaTop < graceMargin && deltaBottom >= 0) || (selfRect.height > bottom && top > bottom)) { + } + + // For mud-popover-bottom-center + else if (classList.contains('mud-popover-bottom-center')) { + // Space available in current vs opposite direction + const spaceAbove = top; + const spaceBelow = window.innerHeight - top; + + if (selfRect.height > spaceAbove && spaceBelow > spaceAbove) { selector = 'bottom'; } - } else if (classList.contains('mud-popover-bottom-right')) { - if ((deltaTop < graceMargin && deltaToLeft < graceMargin && deltaBottom >= 0 && deltaToRight >= selfRect.width) - || (selfRect.height > bottom && selfRect.width > right && top > bottom && left > right)) { + } + + // For mud-popover-bottom-right + else if (classList.contains('mud-popover-bottom-right')) { + // Space available in current direction + const spaceAbove = top; + const spaceLeft = left; + + // Space available in opposite direction + const spaceBelow = window.innerHeight - top; + const spaceRight = window.innerWidth - left; + + // Check if popover exceeds available space AND if opposite side has more space + const shouldFlipVertical = selfRect.height > spaceAbove && spaceBelow > spaceAbove; + const shouldFlipHorizontal = selfRect.width > spaceLeft && spaceRight > spaceLeft; + + if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'bottom-and-right'; - } else if ((deltaTop < graceMargin && deltaBottom >= 0) || (selfRect.height > bottom && top > bottom)) { + } + else if (shouldFlipVertical) { selector = 'bottom'; - } else if ((deltaToLeft < graceMargin && deltaToRight >= selfRect.width) || (selfRect.width > right && left > right)) { + } + else if (shouldFlipHorizontal) { selector = 'right'; } } + } // selector is set in above if statement if it needs to flip @@ -319,8 +401,7 @@ window.mudpopoverHelper = { popoverContentNode.setAttribute('data-mudpopover-flip', 'flipped'); } else { - // did not flip, ensure the left and top are inside bounds - // appbaroffset is another section + // did not flip, ensure the left is inside bounds if (left + offsetX < 0 && // it's starting left of the screen Math.abs(left + offsetX) < selfRect.width) { // it's not starting so far left the entire box would be hidden left = Math.max(0, left + offsetX); @@ -332,25 +413,45 @@ window.mudpopoverHelper = { if (top + offsetY < appBarOffset && appBarElements.length > 0) { this.updatePopoverZIndex(popoverContentNode, appBarElements[0]); - //console.log(`top: ${top} | offsetY: ${offsetY} | total: ${top + offsetY} | appBarOffset: ${appBarOffset}`); } - if (top + offsetY < 0 && // it's starting above the screen - Math.abs(top + offsetY) < selfRect.height) { // it's not starting so far above the entire box would be hidden - top = Math.max(0, top + offsetY); - // set offsetY to 0 to avoid double offset - offsetY = 0; - } + popoverContentNode.removeAttribute('data-mudpopover-flip'); + } - // if it contains a mud-list set that mud-list max-height to be the remaining size on screen - const list = popoverContentNode.querySelector('.mud-list'); - const listPadding = 24; - const listMaxHeight = (window.innerHeight - top - offsetY); - // is list defined and does the list calculated height exceed the listmaxheight - if (list && list.offsetHeight > listMaxHeight) { - list.style.maxHeight = (listMaxHeight - listPadding) + 'px'; + // adjust the popover position/maxheight if it contians a mud-list as it's first descendant + // exceeds the bounds and doesn't have a max-height + const firstChild = popoverContentNode.firstElementChild; + const list = firstChild && firstChild.classList.contains('mud-list') ? firstChild : null; + if (list) { + let anchorPoint = top - offsetY; // Moving upwards + if (offsetY >= 0) { anchorPoint = top + offsetY; } // Moving downwards + // Reset max-height if it was previously set and anchor is in bounds + if (popoverContentNode.mudHeight && anchorPoint > 0 && anchorPoint < window.innerHeight) { + popoverContentNode.style.maxHeight = null; + list.style.maxHeight = null; + popoverContentNode.mudHeight = null; + } + // Check if max-height is set on popover or list + const hasMaxHeight = popoverContentNode.style.maxHeight != '' || list.style.maxHeight != ''; + + if (!hasMaxHeight) { + let contentPadding = 24; + let listMaxHeight = (window.innerHeight - top - offsetY); // moving downwards + + if (top + offsetY < 0 && // it's starting above the screen + Math.abs(top + offsetY) < selfRect.height) { // it's not starting so far above the entire box would be hidden + top = Math.max(contentPadding, top + offsetY); + // set offsetY to 0 to avoid double offset + offsetY = 0; + listMaxHeight = window.innerHeight - top + offsetY; // moving upwards + } + + // if list calculated height exceeds the listmaxheight + if (list.offsetHeight > listMaxHeight && anchorPoint > 0) { + list.style.maxHeight = (listMaxHeight - contentPadding) + 'px'; + popoverContentNode.mudHeight = "setmaxheight"; + } } - popoverContentNode.removeAttribute('data-mudpopover-flip'); } if (classList.contains('mud-popover-overflow-flip-onopen')) { @@ -500,7 +601,7 @@ window.mudpopoverHelper = { (currentNode.scrollHeight > currentNode.clientHeight) || // Vertical scroll (currentNode.scrollWidth > currentNode.clientWidth); // Horizontal scroll if (isScrollable) { - currentNode.addEventListener('scroll', handleScroll, { passive: true }); + currentNode.addEventListener('scroll', debouncedScroll, { passive: true }); scrollableElements.push(currentNode); } // Stop if we reach the body, or head @@ -555,10 +656,7 @@ class MudPopover { // data ticks is not 0 so let's reposition the popover and overlay else if (target.parentNode && target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { - console.log(mutation); - const currTick = Number(target.getAttribute("data-ticks")) || 0; - console.log(`data-ticks: ${currTick} for ${target.id}`); - // reposition popover + // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); // check and reposition overlay if needed let highestTickItem = null; @@ -667,7 +765,7 @@ class MudPopover { const { scrollableElements } = this.map[id]; scrollableElements.forEach(element => { - element.removeEventListener('scroll', handleScroll); + element.removeEventListener('scroll', debouncedScroll); }); // Remove resize observer @@ -699,15 +797,13 @@ class MudPopover { window.mudPopover = new MudPopover(); const debouncedResize = window.mudpopoverHelper.debounce(() => { - console.log("resize"); window.mudpopoverHelper.placePopoverByClassSelector(); }, 25); -const handleScroll = function () { - console.log("scroll"); +const debouncedScroll = window.mudpopoverHelper.debounce(() => { window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); -}; +}, 25); window.addEventListener('resize', debouncedResize, { passive: true }); -window.addEventListener('scroll', handleScroll, { passive: true }); \ No newline at end of file +window.addEventListener('scroll', debouncedScroll, { passive: true }); \ No newline at end of file From d8aeb36ee7b2f6da58fbca33967b9f4d70904964 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Sun, 6 Apr 2025 23:27:27 -0500 Subject: [PATCH 08/22] final adjustments to flipping and mud-list overflow logic --- src/MudBlazor/TScripts/mudPopover.js | 80 +++++++++++++++------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index b6e927e88f94..18c98d436efc 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -141,7 +141,7 @@ window.mudpopoverHelper = { } return { - top: top, left: left, offsetX: offsetX, offsetY: offsetY + top: top, left: left, offsetX: offsetX, offsetY: offsetY, anchorY: top, anchorX: left }; }, @@ -210,6 +210,9 @@ window.mudpopoverHelper = { let top = position.top; // Y-coordinate of the popover let offsetX = position.offsetX; // Horizontal offset of the popover let offsetY = position.offsetY; // Vertical offset of the popover + let anchorY = position.anchorY; // Y-coordinate of the opening anchor + let anchorX = position.anchorX; // X-coordinate of the opening anchor + // get the top/left/ from popoverContentNode if the popover has been hardcoded for position if (classList.contains('mud-popover-position-override')) { left = parseInt(popoverContentNode.style['left']) || left; @@ -236,7 +239,7 @@ window.mudpopoverHelper = { appBarOffset = appBarElements[0].getBoundingClientRect().height; } - const spaceToTop = top - appBarOffset; // Space available above the popover + const contentPadding = 24; // mudPopoverFliped is the flip direction for first flip on flip - onopen popovers let selector = popoverContentNode.mudPopoverFliped; @@ -250,7 +253,7 @@ window.mudpopoverHelper = { const spaceRight = window.innerWidth - left; // Space to the right of the anchor // Space available in opposite direction - const spaceAbove = spaceToTop; + const spaceAbove = top - contentPadding; const spaceLeft = left; // Check if popover exceeds available space AND if opposite side has more space @@ -273,7 +276,7 @@ window.mudpopoverHelper = { else if (classList.contains('mud-popover-top-center')) { // Space available in current direction vs opposite direction const spaceBelow = window.innerHeight - top; - const spaceAbove = spaceToTop; + const spaceAbove = top - contentPadding; // Only flip if popover exceeds available space AND there's more space in opposite direction if (selfRect.height > spaceBelow && spaceAbove > spaceBelow) { @@ -288,7 +291,7 @@ window.mudpopoverHelper = { const spaceLeft = left; // Space available in opposite direction - const spaceAbove = spaceToTop; + const spaceAbove = top - contentPadding; const spaceRight = window.innerWidth - left; // Check if popover exceeds available space AND if opposite side has more space @@ -398,57 +401,62 @@ window.mudpopoverHelper = { top = newPosition.top; offsetX = newPosition.offsetX; offsetY = newPosition.offsetY; - popoverContentNode.setAttribute('data-mudpopover-flip', 'flipped'); + popoverContentNode.setAttribute('data-mudpopover-flip', selector); } else { - // did not flip, ensure the left is inside bounds - if (left + offsetX < 0 && // it's starting left of the screen - Math.abs(left + offsetX) < selfRect.width) { // it's not starting so far left the entire box would be hidden - left = Math.max(0, left + offsetX); - // set offsetX to 0 to avoid double offset - offsetX = 0; - } + popoverContentNode.removeAttribute('data-mudpopover-flip'); + } + + // ensure the left is inside bounds + if (left + offsetX < contentPadding && // it's starting left of the screen + Math.abs(left + offsetX) < selfRect.width) { // it's not starting so far left the entire box would be hidden + left = contentPadding; + // set offsetX to 0 to avoid double offset + offsetX = 0; + } - // will be covered by appbar so adjust zindex with appbar as parent - if (top + offsetY < appBarOffset && - appBarElements.length > 0) { - this.updatePopoverZIndex(popoverContentNode, appBarElements[0]); - } + // ensure the top is inside bounds + if (top + offsetY < contentPadding && // it's starting above the screen + boundingRect.top >= 0 && // the popoverNode is still on screen + Math.abs(top + offsetY) < selfRect.height) { // it's not starting so far above the entire box would be hidden + top = contentPadding; + // set offsetY to 0 to avoid double offset + offsetY = 0; + } - popoverContentNode.removeAttribute('data-mudpopover-flip'); + // will be covered by appbar so adjust zindex with appbar as parent + if (top + offsetY < appBarOffset && + appBarElements.length > 0) { + this.updatePopoverZIndex(popoverContentNode, appBarElements[0]); } // adjust the popover position/maxheight if it contians a mud-list as it's first descendant - // exceeds the bounds and doesn't have a max-height + // exceeds the bounds and doesn't have a max-height set by the user + // maxHeight adjustments stop the minute popoverNode is no longer inside the window const firstChild = popoverContentNode.firstElementChild; const list = firstChild && firstChild.classList.contains('mud-list') ? firstChild : null; if (list) { - let anchorPoint = top - offsetY; // Moving upwards - if (offsetY >= 0) { anchorPoint = top + offsetY; } // Moving downwards // Reset max-height if it was previously set and anchor is in bounds - if (popoverContentNode.mudHeight && anchorPoint > 0 && anchorPoint < window.innerHeight) { + if (popoverContentNode.mudHeight && anchorY > 0 && anchorY < window.innerHeight) { popoverContentNode.style.maxHeight = null; list.style.maxHeight = null; popoverContentNode.mudHeight = null; } + // Check if max-height is set on popover or list const hasMaxHeight = popoverContentNode.style.maxHeight != '' || list.style.maxHeight != ''; - if (!hasMaxHeight) { - let contentPadding = 24; - let listMaxHeight = (window.innerHeight - top - offsetY); // moving downwards - - if (top + offsetY < 0 && // it's starting above the screen - Math.abs(top + offsetY) < selfRect.height) { // it's not starting so far above the entire box would be hidden - top = Math.max(contentPadding, top + offsetY); - // set offsetY to 0 to avoid double offset - offsetY = 0; - listMaxHeight = window.innerHeight - top + offsetY; // moving upwards + if (!hasMaxHeight) { + // calculate list max height if it exceeds bounds + let listMaxHeight = window.innerHeight - top - offsetY; // downwards + // moving upwards + if (top + offsetY < anchorY || top + offsetY == contentPadding) { + listMaxHeight = anchorY - contentPadding; } - + // if list calculated height exceeds the listmaxheight - if (list.offsetHeight > listMaxHeight && anchorPoint > 0) { - list.style.maxHeight = (listMaxHeight - contentPadding) + 'px'; + if (list.offsetHeight > listMaxHeight) { + list.style.maxHeight = (listMaxHeight) + 'px'; popoverContentNode.mudHeight = "setmaxheight"; } } From 97ed340c17fdaa62cbcac0f2cc19417be3846c80 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Mon, 7 Apr 2025 16:17:35 -0500 Subject: [PATCH 09/22] revert tooltip not in dom --- .../Components/Tooltip/MudTooltip.razor | 2 +- .../Components/Tooltip/MudTooltip.razor.cs | 47 ++----------------- 2 files changed, 5 insertions(+), 44 deletions(-) diff --git a/src/MudBlazor/Components/Tooltip/MudTooltip.razor b/src/MudBlazor/Components/Tooltip/MudTooltip.razor index 3a9f2a6a2449..b51243294822 100644 --- a/src/MudBlazor/Components/Tooltip/MudTooltip.razor +++ b/src/MudBlazor/Components/Tooltip/MudTooltip.razor @@ -5,7 +5,7 @@ @ChildContent @if (ShowToolTip()) { - + @if (TooltipContent is not null) {
diff --git a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs index 56f438cee950..d71defc4e02b 100644 --- a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs +++ b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Components; using MudBlazor.State; using MudBlazor.Utilities; -using MudBlazor.Utilities.Debounce; namespace MudBlazor { @@ -11,16 +10,8 @@ public partial class MudTooltip : MudComponentBase private readonly ParameterState _visibleState; private Origin _anchorOrigin; private Origin _transformOrigin; - internal DebounceDispatcher _showDebouncer; - internal DebounceDispatcher _hideDebouncer; - internal double _previousDelay; - internal double _previousDuration; public MudTooltip() { - _previousDelay = Delay; - _showDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Delay)); - _previousDuration = Duration; - _hideDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Duration)); using var registerScope = CreateRegisterScope(); _visibleState = registerScope.RegisterParameter(nameof(Visible)) .WithParameter(() => Visible) @@ -172,49 +163,19 @@ public MudTooltip() ///
internal bool ShowToolTip() { - if (_anchorOrigin == Origin.TopLeft || _transformOrigin == Origin.TopLeft) - ConvertPlacement(); - return !Disabled && _visibleState.Value && (TooltipContent is not null || !string.IsNullOrEmpty(Text)); + return !Disabled && (TooltipContent is not null || !string.IsNullOrEmpty(Text)); } protected override void OnParametersSet() { base.OnParametersSet(); - if (Math.Abs(_previousDelay - Delay) > .001) - { - _showDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Delay)); - _previousDelay = Delay; - } - - if (Math.Abs(_previousDuration - Duration) > .001) - { - _hideDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Duration)); - _previousDuration = Duration; - } + ConvertPlacement(); } - internal Task HandlePointerEnterAsync() - { - if (!ShowOnHover) - { - return Task.CompletedTask; - } + internal Task HandlePointerEnterAsync() => ShowOnHover ? _visibleState.SetValueAsync(true) : Task.CompletedTask; - _hideDebouncer.Cancel(); - return _showDebouncer.DebounceAsync(() => _visibleState.SetValueAsync(true)); - } - - internal Task HandlePointerLeaveAsync() - { - if (!ShowOnHover) - { - return Task.CompletedTask; - } - - _showDebouncer.Cancel(); - return _hideDebouncer.DebounceAsync(() => _visibleState.SetValueAsync(false)); - } + internal Task HandlePointerLeaveAsync() => ShowOnHover ? _visibleState.SetValueAsync(false) : Task.CompletedTask; private Task HandleFocusInAsync() { From 11706b57057aa524903d50c205115ed039c0e36c Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Mon, 7 Apr 2025 16:18:18 -0500 Subject: [PATCH 10/22] add overflowpadding to initialize --- src/MudBlazor/Interop/PopoverJsInterop.cs | 7 ++----- src/MudBlazor/Services/Popover/PopoverOptions.cs | 7 +++++++ src/MudBlazor/Services/Popover/PopoverService.cs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/MudBlazor/Interop/PopoverJsInterop.cs b/src/MudBlazor/Interop/PopoverJsInterop.cs index ea5b6a6d6efd..cc42803a0f0e 100644 --- a/src/MudBlazor/Interop/PopoverJsInterop.cs +++ b/src/MudBlazor/Interop/PopoverJsInterop.cs @@ -2,9 +2,6 @@ // MudBlazor licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.JSInterop; namespace MudBlazor.Interop; @@ -19,9 +16,9 @@ public PopoverJsInterop(IJSRuntime jsRuntime) _jsRuntime = jsRuntime; } - public ValueTask Initialize(string containerClass, int flipMargin, CancellationToken cancellationToken = default) + public ValueTask Initialize(string containerClass, int flipMargin, int overflowPadding, CancellationToken cancellationToken = default) { - return _jsRuntime.InvokeVoidAsyncWithErrorHandling("mudPopover.initialize", cancellationToken, containerClass, flipMargin); + return _jsRuntime.InvokeVoidAsyncWithErrorHandling("mudPopover.initialize", cancellationToken, containerClass, flipMargin, overflowPadding); } public ValueTask Connect(Guid id, CancellationToken cancellationToken = default) diff --git a/src/MudBlazor/Services/Popover/PopoverOptions.cs b/src/MudBlazor/Services/Popover/PopoverOptions.cs index 8a79147b29e3..52a8fdfb5754 100644 --- a/src/MudBlazor/Services/Popover/PopoverOptions.cs +++ b/src/MudBlazor/Services/Popover/PopoverOptions.cs @@ -34,6 +34,13 @@ public class PopoverOptions /// public TimeSpan QueueDelay { get; set; } = TimeSpan.FromSeconds(0.5); + /// + /// Gets or sets the overflow padding for the popover. This is used when adjusting popovers that go off screen at the top or left. + /// It is also used to create max-height for popovers containing a list that will go off screen. + /// The default value is 24 rougly equal to the 8dp margin of material design. + /// + public int OverflowPadding { get; set; } = 24; + /// /// Gets or sets a value indicating whether to throw an exception when a duplicate is encountered. /// The default value is true. diff --git a/src/MudBlazor/Services/Popover/PopoverService.cs b/src/MudBlazor/Services/Popover/PopoverService.cs index 9cfa8af1fdbf..22946678f65a 100644 --- a/src/MudBlazor/Services/Popover/PopoverService.cs +++ b/src/MudBlazor/Services/Popover/PopoverService.cs @@ -327,7 +327,7 @@ private async Task InitializeServiceIfNeededAsync() return; } - await _popoverJsInterop.Initialize(PopoverOptions.ContainerClass, PopoverOptions.FlipMargin, _cancellationToken); + await _popoverJsInterop.Initialize(PopoverOptions.ContainerClass, PopoverOptions.FlipMargin, PopoverOptions.OverflowPadding, _cancellationToken); // Starts in background await _batchExecutor.StartAsync(_cancellationToken); IsInitialized = true; From 17ebfad9e89c82daec7a522cbd3e550ad1be65f7 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Mon, 7 Apr 2025 16:18:32 -0500 Subject: [PATCH 11/22] refactor, add overflow from options --- .../Components/PopoverTests.cs | 3 +- src/MudBlazor/TScripts/mudPopover.js | 288 ++++++++++++------ 2 files changed, 194 insertions(+), 97 deletions(-) diff --git a/src/MudBlazor.UnitTests/Components/PopoverTests.cs b/src/MudBlazor.UnitTests/Components/PopoverTests.cs index a7d9e238eab5..07e94aace1e5 100644 --- a/src/MudBlazor.UnitTests/Components/PopoverTests.cs +++ b/src/MudBlazor.UnitTests/Components/PopoverTests.cs @@ -14,7 +14,8 @@ public void PopoverOptions_Defaults() { var options = new PopoverOptions(); - options.ContainerClass.Should().Be("mudblazor-main-content"); + options.OverflowPadding.Should().Be(24); + options.ContainerClass.Should().Be("mud-popover-provider"); options.FlipMargin.Should().Be(0); options.ThrowOnDuplicateProvider.Should().Be(true); } diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 18c98d436efc..24da7a649b0b 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -5,8 +5,7 @@ window.mudpopoverHelper = { // set by the class MudPopover in initialize mainContainerClass: null, - - // set by the class MudPopover in initialize + overflowPadding: 24, flipMargin: 0, // used for setting a debounce @@ -174,34 +173,38 @@ window.mudpopoverHelper = { const popoverContentNode = document.getElementById('popovercontent-' + id); // if the popover doesn't exist we stop - if (!popoverContentNode) { - return; - } + if (!popoverContentNode) return; + const classList = popoverContentNode.classList; // if the popover isn't open we stop - if (classList.contains('mud-popover-open') == false) { - return; - } + if (!classList.contains('mud-popover-open')) return; // if a classSelector was supplied and doesn't exist we stop - if (classSelector) { - if (classList.contains(classSelector) == false) { - return; - } - } + if (classSelector && !classList.contains(classSelector)) return; + + // Batch DOM reads let boundingRect = popoverNode.parentNode.getBoundingClientRect(); + const selfRect = popoverContentNode.getBoundingClientRect(); + const popoverNodeStyle = window.getComputedStyle(popoverNode); + const isPositionFixed = popoverNodeStyle.position === 'fixed'; + const isPositionOverride = classList.contains('mud-popover-position-override'); + const isRelativeWidth = classList.contains('mud-popover-relative-width'); + const isAdaptiveWidth = classList.contains('mud-popover-adaptive-width'); + const isFlipOnOpen = classList.contains('mud-popover-overflow-flip-onopen'); + const isFlipAlways = classList.contains('mud-popover-overflow-flip-always'); + const zIndexAuto = popoverNodeStyle.getPropertyValue('z-index') === 'auto'; + // allow them to be changed after initial creation popoverContentNode.style['max-width'] = 'none'; popoverContentNode.style['min-width'] = 'none'; - if (classList.contains('mud-popover-relative-width')) { + if (isRelativeWidth) { popoverContentNode.style['max-width'] = (boundingRect.width) + 'px'; } - else if (classList.contains('mud-popover-adaptive-width')) { + else if (isAdaptiveWidth) { popoverContentNode.style['min-width'] = (boundingRect.width) + 'px'; } - - const selfRect = popoverContentNode.getBoundingClientRect(); + const classListArray = Array.from(classList); // calculate position based on opening anchor/transform @@ -214,7 +217,7 @@ window.mudpopoverHelper = { let anchorX = position.anchorX; // X-coordinate of the opening anchor // get the top/left/ from popoverContentNode if the popover has been hardcoded for position - if (classList.contains('mud-popover-position-override')) { + if (isPositionOverride) { left = parseInt(popoverContentNode.style['left']) || left; top = parseInt(popoverContentNode.style['top']) || top; // no offset when hardcoded @@ -231,7 +234,7 @@ window.mudpopoverHelper = { }; } // flipping logic - if (classList.contains('mud-popover-overflow-flip-onopen') || classList.contains('mud-popover-overflow-flip-always')) { + if (isFlipOnOpen || isFlipAlways) { const appBarElements = document.getElementsByClassName("mud-appbar mud-appbar-fixed-top"); let appBarOffset = 0; @@ -239,7 +242,6 @@ window.mudpopoverHelper = { appBarOffset = appBarElements[0].getBoundingClientRect().height; } - const contentPadding = 24; // mudPopoverFliped is the flip direction for first flip on flip - onopen popovers let selector = popoverContentNode.mudPopoverFliped; @@ -253,7 +255,7 @@ window.mudpopoverHelper = { const spaceRight = window.innerWidth - left; // Space to the right of the anchor // Space available in opposite direction - const spaceAbove = top - contentPadding; + const spaceAbove = top - window.mudpopoverHelper.overflowPadding; const spaceLeft = left; // Check if popover exceeds available space AND if opposite side has more space @@ -276,7 +278,7 @@ window.mudpopoverHelper = { else if (classList.contains('mud-popover-top-center')) { // Space available in current direction vs opposite direction const spaceBelow = window.innerHeight - top; - const spaceAbove = top - contentPadding; + const spaceAbove = top - window.mudpopoverHelper.overflowPadding; // Only flip if popover exceeds available space AND there's more space in opposite direction if (selfRect.height > spaceBelow && spaceAbove > spaceBelow) { @@ -291,7 +293,7 @@ window.mudpopoverHelper = { const spaceLeft = left; // Space available in opposite direction - const spaceAbove = top - contentPadding; + const spaceAbove = top - window.mudpopoverHelper.overflowPadding; const spaceRight = window.innerWidth - left; // Check if popover exceeds available space AND if opposite side has more space @@ -408,18 +410,18 @@ window.mudpopoverHelper = { } // ensure the left is inside bounds - if (left + offsetX < contentPadding && // it's starting left of the screen + if (left + offsetX < window.mudpopoverHelper.overflowPadding && // it's starting left of the screen Math.abs(left + offsetX) < selfRect.width) { // it's not starting so far left the entire box would be hidden - left = contentPadding; + left = window.mudpopoverHelper.overflowPadding; // set offsetX to 0 to avoid double offset offsetX = 0; } // ensure the top is inside bounds - if (top + offsetY < contentPadding && // it's starting above the screen + if (top + offsetY < window.mudpopoverHelper.overflowPadding && // it's starting above the screen boundingRect.top >= 0 && // the popoverNode is still on screen Math.abs(top + offsetY) < selfRect.height) { // it's not starting so far above the entire box would be hidden - top = contentPadding; + top = window.mudpopoverHelper.overflowPadding; // set offsetY to 0 to avoid double offset offsetY = 0; } @@ -447,29 +449,38 @@ window.mudpopoverHelper = { const hasMaxHeight = popoverContentNode.style.maxHeight != '' || list.style.maxHeight != ''; if (!hasMaxHeight) { + // in case of a reflow check if should show from top properly + let shouldShowFromTop = false; // calculate list max height if it exceeds bounds - let listMaxHeight = window.innerHeight - top - offsetY; // downwards + let listMaxHeight = window.innerHeight - top - offsetY - window.mudpopoverHelper.overflowPadding; // downwards // moving upwards - if (top + offsetY < anchorY || top + offsetY == contentPadding) { - listMaxHeight = anchorY - contentPadding; + if (top + offsetY < anchorY || top + offsetY == window.mudpopoverHelper.overflowPadding) { + console.log("up set"); + shouldShowFromTop = true; + listMaxHeight = anchorY - window.mudpopoverHelper.overflowPadding; } + else console.log("down set"); // if list calculated height exceeds the listmaxheight if (list.offsetHeight > listMaxHeight) { + if (shouldShowFromTop) { // adjust top to show from top + top = window.mudpopoverHelper.overflowPadding; + offsetY = 0; + } list.style.maxHeight = (listMaxHeight) + 'px'; popoverContentNode.mudHeight = "setmaxheight"; } } } - if (classList.contains('mud-popover-overflow-flip-onopen')) { + if (isFlipOnOpen) { // store flip direction on open so it's not recalculated if (!popoverContentNode.mudPopoverFliped) { popoverContentNode.mudPopoverFliped = selector || 'none'; } } } - if (window.getComputedStyle(popoverNode).position == 'fixed') { + if (isPositionFixed) { popoverContentNode.style['position'] = 'fixed'; } else if (!classList.contains('mud-popover-fixed')) { @@ -477,7 +488,7 @@ window.mudpopoverHelper = { offsetY += window.scrollY } - if (classList.contains('mud-popover-position-override')) { + if (isPositionOverride) { // no offset if popover position is hardcoded offsetX = 0; offsetY = 0; @@ -491,8 +502,8 @@ window.mudpopoverHelper = { //console.log(popoverContentNode, popoverNode.parentNode); this.updatePopoverZIndex(popoverContentNode, popoverNode.parentNode); - if (window.getComputedStyle(popoverNode).getPropertyValue('z-index') != 'auto') { - popoverContentNode.style['z-index'] = Math.max(window.getComputedStyle(popoverNode).getPropertyValue('z-index'), popoverContentNode.style['z-index']); + if (!zIndexAuto) { + popoverContentNode.style['z-index'] = Math.max(popoverNodeStyle.getPropertyValue('z-index'), popoverContentNode.style['z-index']); popoverContentNode.skipZIndex = true; } } @@ -609,7 +620,7 @@ window.mudpopoverHelper = { (currentNode.scrollHeight > currentNode.clientHeight) || // Vertical scroll (currentNode.scrollWidth > currentNode.clientWidth); // Horizontal scroll if (isScrollable) { - currentNode.addEventListener('scroll', debouncedScroll, { passive: true }); + currentNode.addEventListener('scroll', window.mudpopoverHelper.debouncedScroll, { passive: true }); scrollableElements.push(currentNode); } // Stop if we reach the body, or head @@ -629,18 +640,86 @@ class MudPopover { this.contentObserver = null; } + createObservers(id) { + + // this is the origin of the popover in the dom, it can be nested inside another popover's content + // e.g. the filter popover for datagrid, this would be the inside of where the mudpopover was placed + // popoverNode.parentNode is it's immediate parent or the actual element in the above example + const popoverNode = document.getElementById('popover-' + id); + + // this is the content node in the provider regardless of the RenderFragment that exists when the popover is active + const popoverContentNode = document.getElementById('popovercontent-' + id); + + if (popoverNode && popoverNode.parentNode && popoverContentNode) { + // Add scroll event listeners to the content node and its parents up to the Body + const scrollableElements = window.mudpopoverHelper.popoverScrollListener(popoverNode); + + // add a resize observor to catch resize events + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + const target = entry.target; + for (const childNode of target.childNodes) { + if (childNode.id && childNode.id.startsWith('popover-')) { + window.mudpopoverHelper.debouncedResize(); + } + } + } + }); + + resizeObserver.observe(popoverNode.parentNode); + + // Store all references needed for later cleanup + this.map[id] = { + popoverContentNode: popoverContentNode, + scrollableElements: scrollableElements, + parentResizeObserver: resizeObserver, + isOpened: false + }; + } else { + console.warn(`Could not connect observers to popover with ID ${id}: One or more required elements not found`); + } + } + + disposeObservers(id) { + // Get references to items that need cleanup + const { scrollableElements, parentResizeObserver } = this.map[id]; + + // 1. Remove scroll event listeners from all scrollable parent elements + if (scrollableElements && Array.isArray(scrollableElements)) { + scrollableElements.forEach(element => { + if (element && typeof element.removeEventListener === 'function') { + element.removeEventListener('scroll', window.mudpopoverHelper.debouncedScroll); + } + }); + } + + // 2. Disconnect any resize observers + if (parentResizeObserver && typeof parentResizeObserver.disconnect === 'function') { + parentResizeObserver.disconnect(); + } + + // 3. Clear references to allow garbage collection + this.map[id].scrollableElements = null; + this.map[id].parentResizeObserver = null; + } + callbackPopover(mutation) { const target = mutation.target; if (!target) return; - // we use top and left negative numbers to prevent showing until done with this method if (mutation.type == 'attributes' && mutation.attributeName == 'data-ticks') { // when data-ticks attribute is the mutation something has changed with the popover // and it needs to be repositioned and shown, note we don't use mud-popover-open here // instead we use data-ticks since we know the newest data-ticks > 0 is the top most. const tickAttribute = target.getAttribute('data-ticks'); + const id = target.id.substr(15); // if data-ticks is 0 the popover isn't open and it's hidden in css but we don't want it to reappear until // it's positioned the next time so we move it off screen if (tickAttribute == 0) { + // tell the map that this popover is closed + if (this.map[id] && this.map[id].isOpened) { + this.map[id].isOpened = false; + this.disposeObservers(id); + } // wait this long until we "move it off screen" const delay = parseFloat(target.style['transition-duration']) || 0; if (delay == 0) { @@ -656,14 +735,15 @@ class MudPopover { // reset flip status target.mudPopoverFliped = null; target.removeAttribute('data-mudpopover-flip'); - - // tell the map that this popover is closed - const id = target.id.substr(15); - this.map[id].isOpened = false; } // data ticks is not 0 so let's reposition the popover and overlay else if (target.parentNode && target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { + const id = target.id.substr(15); + if (this.map[id] && !this.map[id].isOpened) { + this.map[id].isOpened = true; + this.createObservers(id); + } // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); // check and reposition overlay if needed @@ -686,11 +766,10 @@ class MudPopover { window.mudpopoverHelper.updatePopoverOverlay(highestTickItem); } } - } } - initialize(containerClass, flipMargin) { + initialize(containerClass, flipMargin, overflowPadding) { // only happens when the PopoverService is created which happens on application start and anytime the service might crash const mainContent = document.getElementsByClassName(containerClass); if (mainContent.length == 0) { @@ -699,6 +778,7 @@ class MudPopover { } // store options from PopoverOptions in mudpopoverHelper window.mudpopoverHelper.mainContainerClass = containerClass; + window.mudpopoverHelper.overflowPadding = overflowPadding; if (flipMargin) { window.mudpopoverHelper.flipMargin = flipMargin; @@ -713,24 +793,41 @@ class MudPopover { attributeFilter: ['data-ticks'] // limit to just data-ticks }; + // Dispose of any existing observer before creating a new one + if (this.contentObserver) { + this.contentObserver.disconnect(); + this.contentObserver = null; + } + const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { // if it's direct parent is the provider // and contains the class mud-popover if (mutation.target.parentNode === provider && mutation.target.classList.contains('mud-popover')) { this.callbackPopover(mutation); - } + } } }); observer.observe(provider, config); // store it so we can dispose of it properly this.contentObserver = observer; + + // setup event listeners + window.addEventListener('resize', window.mudpopoverHelper.debouncedResize, { passive: true }); + window.addEventListener('scroll', window.mudpopoverHelper.debouncedScroll, { passive: true }); } + /** + * Connects a popover element to the system, setting up all necessary event listeners and observers + * @param {string} id - The ID of the popover to connect + */ connect(id) { // this happens when a popover is created in the dom (not necessarily displayed) - // removed extra initialize and extra scroll listener that attached to the provider and body for every popover + // Ensure we're not creating duplicate listeners for the same ID + if (this.map[id]) { + this.disconnect(id); + } // this is the origin of the popover in the dom, it can be nested inside another popover's content // e.g. the filter popover for datagrid, this would be the inside of where the mudpopover was placed @@ -740,78 +837,77 @@ class MudPopover { // this is the content node in the provider regardless of the RenderFragment that exists when the popover is active const popoverContentNode = document.getElementById('popovercontent-' + id); - if (popoverNode && popoverNode.parentNode && popoverContentNode) { - - // Add scroll event listeners to the content node and its parents up to the Body - const scrollableElements = window.mudpopoverHelper.popoverScrollListener(popoverNode); - - // add a resize observor to catch resize events - const resizeObserver = new ResizeObserver(entries => { - for (let entry of entries) { - const target = entry.target; - for (const childNode of target.childNodes) { - if (childNode.id && childNode.id.startsWith('popover-')) { - debouncedResize(); - } - } - } - }); - - resizeObserver.observe(popoverNode.parentNode); - - this.map[id] = { - popoverContentNode: popoverContentNode, - scrollableElements: scrollableElements, - parentResizeObserver: resizeObserver, - }; - } + // Store all references needed for later cleanup + this.map[id] = { + popoverContentNode: popoverContentNode, + isOpened: false + }; } + /** + * Disconnects a popover element, properly cleaning up all event listeners and observers + * @param {string} id - The ID of the popover to disconnect + */ disconnect(id) { - if (this.map[id]) { - // Remove scroll event listeners from the stored scrollable elements - const { scrollableElements } = this.map[id]; - - scrollableElements.forEach(element => { - element.removeEventListener('scroll', debouncedScroll); - }); + if (!this.map[id]) { + return; // Nothing to disconnect + } + + try { + // 1. Remove individual observers and listeners that might exist + this.disposeObservers(id); - // Remove resize observer - this.map[id].parentResizeObserver.disconnect(); + // 2. Clear final reference to allow garbage collection + this.map[id].popoverContentNode = null; + // 3. Remove this entry from the map delete this.map[id]; + } catch (error) { + console.error(`Error disconnecting popover with ID ${id}:`, error); } } + /** + * Disposes all resources used by this MudPopover instance + * Should be called when the component is being unmounted + */ dispose() { - for (var i in this.map) { - this.disconnect(i); - } + try { + // 1. Disconnect all popovers + const ids = Object.keys(this.map); + for (const id of ids) { + this.disconnect(id); + } - this.contentObserver.disconnect(); - this.contentObserver = null; - } + // 2. Ensure map is empty + this.map = {}; - getAllObservedContainers() { - const result = []; - for (var i in this.map) { - result.push(i); + // 3. Disconnect the content observer + if (this.contentObserver) { + this.contentObserver.disconnect(); + this.contentObserver = null; + } + + // 4. Remove global event listeners (handled outside this class, listed here for reference) + window.removeEventListener('resize', window.mudpopoverHelper.debouncedResize); + window.removeEventListener('scroll', window.mudpopoverHelper.debouncedScroll); + } catch (error) { + console.error("Error disposing MudPopover:", error); } + } - return result; + getAllObservedContainers() { + return Object.keys(this.map); } } -window.mudPopover = new MudPopover(); - -const debouncedResize = window.mudpopoverHelper.debounce(() => { +window.mudpopoverHelper.debouncedResize = window.mudpopoverHelper.debounce(() => { window.mudpopoverHelper.placePopoverByClassSelector(); }, 25); -const debouncedScroll = window.mudpopoverHelper.debounce(() => { +window.mudpopoverHelper.debouncedScroll = window.mudpopoverHelper.debounce(() => { window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); }, 25); -window.addEventListener('resize', debouncedResize, { passive: true }); -window.addEventListener('scroll', debouncedScroll, { passive: true }); \ No newline at end of file +window.mudPopover = new MudPopover(); \ No newline at end of file From 507e90cd2d8b0ab6479731da88778c4c49d841a6 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Mon, 7 Apr 2025 17:00:16 -0500 Subject: [PATCH 12/22] re-ordered initial flow and delayed hide --- src/MudBlazor/TScripts/mudPopover.js | 61 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 24da7a649b0b..48d9257cb16a 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -706,16 +706,21 @@ class MudPopover { callbackPopover(mutation) { const target = mutation.target; if (!target) return; - if (mutation.type == 'attributes' && mutation.attributeName == 'data-ticks') { - // when data-ticks attribute is the mutation something has changed with the popover - // and it needs to be repositioned and shown, note we don't use mud-popover-open here - // instead we use data-ticks since we know the newest data-ticks > 0 is the top most. - const tickAttribute = target.getAttribute('data-ticks'); - const id = target.id.substr(15); - // if data-ticks is 0 the popover isn't open and it's hidden in css but we don't want it to reappear until - // it's positioned the next time so we move it off screen - if (tickAttribute == 0) { - // tell the map that this popover is closed + const id = target.id.substr(15); + if (mutation.type == 'attributes' && mutation.attributeName == 'class') { + if (target.classList.contains('mud-popover-open')) { + // setup for an open popover and create observers + if (this.map[id] && !this.map[id].isOpened) { + this.map[id].isOpened = true; + this.createObservers(id); + } + console.log("open"); + // reposition popover individually + window.mudpopoverHelper.placePopoverByNode(target); + } + else { + // tell the map that this popover is closed + if (this.map[id] && this.map[id].isOpened) { this.map[id].isOpened = false; this.disposeObservers(id); @@ -726,27 +731,37 @@ class MudPopover { // remove left and top styles target.style.removeProperty('left'); target.style.removeProperty('top'); + console.log("immediate hide"); + } + else { + setTimeout(() => { + if (this.map[id] && this.map[id].isOpened) return; // in case it's reopened before the timeout is over + if (target && !target.classList.contains('mud-popover-open')) { + target.style.removeProperty('left'); + target.style.removeProperty('top'); + console.log("delayed hide"); + } + }, delay); } - setTimeout(() => { - target.style.removeProperty('left'); - target.style.removeProperty('top'); - }, delay); - // reset flip status target.mudPopoverFliped = null; target.removeAttribute('data-mudpopover-flip'); + console.log("close"); } + } + else if (mutation.type == 'attributes' && mutation.attributeName == 'data-ticks') { + // when data-ticks attribute is the mutation something has changed with the popover + // and it needs to be repositioned and shown, note we don't use mud-popover-open here + // instead we use data-ticks since we know the newest data-ticks > 0 is the top most. + const tickAttribute = target.getAttribute('data-ticks'); // data ticks is not 0 so let's reposition the popover and overlay - else if (target.parentNode && + if (tickAttribute > 0 && target.parentNode && target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { - const id = target.id.substr(15); - if (this.map[id] && !this.map[id].isOpened) { - this.map[id].isOpened = true; - this.createObservers(id); - } + // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); - // check and reposition overlay if needed + + // check and reposition overlay if needed, positions and z-index can change during a reflow so leave it in data-ticks let highestTickItem = null; let highestTickValue = -1; @@ -790,7 +805,7 @@ class MudPopover { const config = { attributes: true, // only observe attributes subtree: true, // all descendants of popover - attributeFilter: ['data-ticks'] // limit to just data-ticks + attributeFilter: ['data-ticks','class'] // limit to just data-ticks and class changes }; // Dispose of any existing observer before creating a new one From 30fdd110425a6e1d1a36a15a9ba5a4063936dd48 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Mon, 7 Apr 2025 19:23:12 -0500 Subject: [PATCH 13/22] remove extra console messages --- src/MudBlazor/TScripts/mudPopover.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 48d9257cb16a..bafbe74911be 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -455,11 +455,9 @@ window.mudpopoverHelper = { let listMaxHeight = window.innerHeight - top - offsetY - window.mudpopoverHelper.overflowPadding; // downwards // moving upwards if (top + offsetY < anchorY || top + offsetY == window.mudpopoverHelper.overflowPadding) { - console.log("up set"); shouldShowFromTop = true; listMaxHeight = anchorY - window.mudpopoverHelper.overflowPadding; } - else console.log("down set"); // if list calculated height exceeds the listmaxheight if (list.offsetHeight > listMaxHeight) { @@ -499,7 +497,6 @@ window.mudpopoverHelper = { // update z-index by sending the calling popover to update z-index, // and the parentnode of the calling popover (not content parent) - //console.log(popoverContentNode, popoverNode.parentNode); this.updatePopoverZIndex(popoverContentNode, popoverNode.parentNode); if (!zIndexAuto) { @@ -714,7 +711,6 @@ class MudPopover { this.map[id].isOpened = true; this.createObservers(id); } - console.log("open"); // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); } @@ -731,7 +727,6 @@ class MudPopover { // remove left and top styles target.style.removeProperty('left'); target.style.removeProperty('top'); - console.log("immediate hide"); } else { setTimeout(() => { @@ -739,14 +734,12 @@ class MudPopover { if (target && !target.classList.contains('mud-popover-open')) { target.style.removeProperty('left'); target.style.removeProperty('top'); - console.log("delayed hide"); } }, delay); } // reset flip status target.mudPopoverFliped = null; target.removeAttribute('data-mudpopover-flip'); - console.log("close"); } } else if (mutation.type == 'attributes' && mutation.attributeName == 'data-ticks') { From c7976e2871959caf2136e1231095a0ceb128969d Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Tue, 8 Apr 2025 00:14:53 -0500 Subject: [PATCH 14/22] change to flip margin and get properheight and width from contentnode directly since we are modifying width on the fly. remove scroll from being debounced. set observers to connect on open and disconnect on close. --- src/MudBlazor/TScripts/mudPopover.js | 104 ++++++++++++++------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index bafbe74911be..546a3bf86917 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -247,20 +247,22 @@ window.mudpopoverHelper = { // flip routine off transform origin, sets selector to an axis to flip on if needed if (!selector) { - + const popoverHeight = popoverContentNode.offsetHeight; + const popoverWidth = popoverContentNode.offsetWidth; // For mud-popover-top-left + if (classList.contains('mud-popover-top-left')) { // Space available in current direction - const spaceBelow = window.innerHeight - top; // Space below the anchor - const spaceRight = window.innerWidth - left; // Space to the right of the anchor + const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin; // Space below the anchor + const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin; // Space to the right of the anchor // Space available in opposite direction - const spaceAbove = top - window.mudpopoverHelper.overflowPadding; - const spaceLeft = left; + const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin; + const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin; // Check if popover exceeds available space AND if opposite side has more space - const shouldFlipVertical = selfRect.height > spaceBelow && spaceAbove > spaceBelow; - const shouldFlipHorizontal = selfRect.width > spaceRight && spaceLeft > spaceRight; + const shouldFlipVertical = popoverHeight > spaceBelow && spaceAbove > spaceBelow; + const shouldFlipHorizontal = popoverWidth > spaceRight && spaceLeft > spaceRight; // Apply flips based on space comparisons if (shouldFlipVertical && shouldFlipHorizontal) { @@ -277,11 +279,11 @@ window.mudpopoverHelper = { // For mud-popover-top-center else if (classList.contains('mud-popover-top-center')) { // Space available in current direction vs opposite direction - const spaceBelow = window.innerHeight - top; - const spaceAbove = top - window.mudpopoverHelper.overflowPadding; + const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin; + const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin; // Only flip if popover exceeds available space AND there's more space in opposite direction - if (selfRect.height > spaceBelow && spaceAbove > spaceBelow) { + if (popoverHeight > spaceBelow && spaceAbove > spaceBelow) { selector = 'top'; } } @@ -289,16 +291,16 @@ window.mudpopoverHelper = { // For mud-popover-top-right else if (classList.contains('mud-popover-top-right')) { // Space available in current direction - const spaceBelow = window.innerHeight - top; - const spaceLeft = left; + const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin; + const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin; // Space available in opposite direction - const spaceAbove = top - window.mudpopoverHelper.overflowPadding; - const spaceRight = window.innerWidth - left; + const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin; + const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin; // Check if popover exceeds available space AND if opposite side has more space - const shouldFlipVertical = selfRect.height > spaceBelow && spaceAbove > spaceBelow; - const shouldFlipHorizontal = selfRect.width > spaceLeft && spaceRight > spaceLeft; + const shouldFlipVertical = popoverHeight > spaceBelow && spaceAbove > spaceBelow; + const shouldFlipHorizontal = popoverWidth > spaceLeft && spaceRight > spaceLeft; if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'top-and-right'; @@ -314,10 +316,10 @@ window.mudpopoverHelper = { // For mud-popover-center-left else if (classList.contains('mud-popover-center-left')) { // Space available in current vs opposite direction - const spaceRight = window.innerWidth - left; - const spaceLeft = left; + const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin; + const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin; - if (selfRect.width > spaceRight && spaceLeft > spaceRight) { + if (popoverWidth > spaceRight && spaceLeft > spaceRight) { selector = 'left'; } } @@ -325,10 +327,10 @@ window.mudpopoverHelper = { // For mud-popover-center-right else if (classList.contains('mud-popover-center-right')) { // Space available in current vs opposite direction - const spaceLeft = left; - const spaceRight = window.innerWidth - left; + const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin; + const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin; - if (selfRect.width > spaceLeft && spaceRight > spaceLeft) { + if (popoverWidth > spaceLeft && spaceRight > spaceLeft) { selector = 'right'; } } @@ -336,16 +338,16 @@ window.mudpopoverHelper = { // For mud-popover-bottom-left else if (classList.contains('mud-popover-bottom-left')) { // Space available in current direction - const spaceAbove = top; - const spaceRight = window.innerWidth - left; + const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin; + const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin; // Space available in opposite direction - const spaceBelow = window.innerHeight - top; - const spaceLeft = left; + const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin; + const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin; // Check if popover exceeds available space AND if opposite side has more space - const shouldFlipVertical = selfRect.height > spaceAbove && spaceBelow > spaceAbove; - const shouldFlipHorizontal = selfRect.width > spaceRight && spaceLeft > spaceRight; + const shouldFlipVertical = popoverHeight > spaceAbove && spaceBelow > spaceAbove; + const shouldFlipHorizontal = popoverWidth > spaceRight && spaceLeft > spaceRight; if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'bottom-and-left'; @@ -361,10 +363,10 @@ window.mudpopoverHelper = { // For mud-popover-bottom-center else if (classList.contains('mud-popover-bottom-center')) { // Space available in current vs opposite direction - const spaceAbove = top; - const spaceBelow = window.innerHeight - top; + const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin; + const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin; - if (selfRect.height > spaceAbove && spaceBelow > spaceAbove) { + if (popoverHeight > spaceAbove && spaceBelow > spaceAbove) { selector = 'bottom'; } } @@ -372,16 +374,16 @@ window.mudpopoverHelper = { // For mud-popover-bottom-right else if (classList.contains('mud-popover-bottom-right')) { // Space available in current direction - const spaceAbove = top; - const spaceLeft = left; + const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin; + const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin; // Space available in opposite direction - const spaceBelow = window.innerHeight - top; - const spaceRight = window.innerWidth - left; + const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin; + const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin; // Check if popover exceeds available space AND if opposite side has more space - const shouldFlipVertical = selfRect.height > spaceAbove && spaceBelow > spaceAbove; - const shouldFlipHorizontal = selfRect.width > spaceLeft && spaceRight > spaceLeft; + const shouldFlipVertical = popoverHeight > spaceAbove && spaceBelow > spaceAbove; + const shouldFlipHorizontal = popoverWidth > spaceLeft && spaceRight > spaceLeft; if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'bottom-and-right'; @@ -617,7 +619,7 @@ window.mudpopoverHelper = { (currentNode.scrollHeight > currentNode.clientHeight) || // Vertical scroll (currentNode.scrollWidth > currentNode.clientWidth); // Horizontal scroll if (isScrollable) { - currentNode.addEventListener('scroll', window.mudpopoverHelper.debouncedScroll, { passive: true }); + currentNode.addEventListener('scroll', window.mudpopoverHelper.handleScroll, { passive: true }); scrollableElements.push(currentNode); } // Stop if we reach the body, or head @@ -685,7 +687,7 @@ class MudPopover { if (scrollableElements && Array.isArray(scrollableElements)) { scrollableElements.forEach(element => { if (element && typeof element.removeEventListener === 'function') { - element.removeEventListener('scroll', window.mudpopoverHelper.debouncedScroll); + element.removeEventListener('scroll', window.mudpopoverHelper.handleScroll); } }); } @@ -708,18 +710,17 @@ class MudPopover { if (target.classList.contains('mud-popover-open')) { // setup for an open popover and create observers if (this.map[id] && !this.map[id].isOpened) { - this.map[id].isOpened = true; - this.createObservers(id); - } + this.map[id].isOpened = true; + } + // create observers for this popover (resizeObserver and scroll Listeners) + this.createObservers(id); // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); } else { - // tell the map that this popover is closed - + // tell the map that this popover is closed if (this.map[id] && this.map[id].isOpened) { this.map[id].isOpened = false; - this.disposeObservers(id); } // wait this long until we "move it off screen" const delay = parseFloat(target.style['transition-duration']) || 0; @@ -740,6 +741,9 @@ class MudPopover { // reset flip status target.mudPopoverFliped = null; target.removeAttribute('data-mudpopover-flip'); + + // Remove individual observers and listeners that might exist + this.disposeObservers(id); } } else if (mutation.type == 'attributes' && mutation.attributeName == 'data-ticks') { @@ -748,7 +752,7 @@ class MudPopover { // instead we use data-ticks since we know the newest data-ticks > 0 is the top most. const tickAttribute = target.getAttribute('data-ticks'); // data ticks is not 0 so let's reposition the popover and overlay - if (tickAttribute > 0 && target.parentNode && + if (tickAttribute > 0 && target.parentNode && this.map[id] && this.map[id].isOpened && target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { // reposition popover individually @@ -823,7 +827,7 @@ class MudPopover { // setup event listeners window.addEventListener('resize', window.mudpopoverHelper.debouncedResize, { passive: true }); - window.addEventListener('scroll', window.mudpopoverHelper.debouncedScroll, { passive: true }); + window.addEventListener('scroll', window.mudpopoverHelper.handleScroll, { passive: true }); } /** @@ -898,7 +902,7 @@ class MudPopover { // 4. Remove global event listeners (handled outside this class, listed here for reference) window.removeEventListener('resize', window.mudpopoverHelper.debouncedResize); - window.removeEventListener('scroll', window.mudpopoverHelper.debouncedScroll); + window.removeEventListener('scroll', window.mudpopoverHelper.handleScroll); } catch (error) { console.error("Error disposing MudPopover:", error); } @@ -913,9 +917,9 @@ window.mudpopoverHelper.debouncedResize = window.mudpopoverHelper.debounce(() => window.mudpopoverHelper.placePopoverByClassSelector(); }, 25); -window.mudpopoverHelper.debouncedScroll = window.mudpopoverHelper.debounce(() => { +window.mudpopoverHelper.handleScroll = function() { window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); -}, 25); +}; window.mudPopover = new MudPopover(); \ No newline at end of file From b0e1101e39de1df3c5a66e6d583ed108a225fda7 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Tue, 8 Apr 2025 09:27:33 -0500 Subject: [PATCH 15/22] state save, reordered a few things for smoother processing --- src/MudBlazor/TScripts/mudPopover.js | 75 +++++++++++++++------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index 546a3bf86917..d23766f23734 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -194,17 +194,6 @@ window.mudpopoverHelper = { const isFlipOnOpen = classList.contains('mud-popover-overflow-flip-onopen'); const isFlipAlways = classList.contains('mud-popover-overflow-flip-always'); const zIndexAuto = popoverNodeStyle.getPropertyValue('z-index') === 'auto'; - - // allow them to be changed after initial creation - popoverContentNode.style['max-width'] = 'none'; - popoverContentNode.style['min-width'] = 'none'; - if (isRelativeWidth) { - popoverContentNode.style['max-width'] = (boundingRect.width) + 'px'; - } - else if (isAdaptiveWidth) { - popoverContentNode.style['min-width'] = (boundingRect.width) + 'px'; - } - const classListArray = Array.from(classList); // calculate position based on opening anchor/transform @@ -216,6 +205,28 @@ window.mudpopoverHelper = { let anchorY = position.anchorY; // Y-coordinate of the opening anchor let anchorX = position.anchorX; // X-coordinate of the opening anchor + // reset widths and allow them to be changed after initial creation + popoverContentNode.style['max-width'] = 'none'; + popoverContentNode.style['min-width'] = 'none'; + if (isRelativeWidth) { + popoverContentNode.style['max-width'] = (boundingRect.width) + 'px'; + } + else if (isAdaptiveWidth) { + popoverContentNode.style['min-width'] = (boundingRect.width) + 'px'; + } + + // reset heights and allow them to be changed after initial creation + const firstChild = popoverContentNode.firstElementChild; + const list = firstChild && firstChild.classList.contains('mud-list') ? firstChild : null; + if (list) { + // Reset max-height if it was previously set and anchor is in bounds + if (popoverContentNode.mudHeight && anchorY > 0 && anchorY < window.innerHeight) { + popoverContentNode.style.maxHeight = null; + list.style.maxHeight = null; + popoverContentNode.mudHeight = null; + } + } + // get the top/left/ from popoverContentNode if the popover has been hardcoded for position if (isPositionOverride) { left = parseInt(popoverContentNode.style['left']) || left; @@ -263,7 +274,6 @@ window.mudpopoverHelper = { // Check if popover exceeds available space AND if opposite side has more space const shouldFlipVertical = popoverHeight > spaceBelow && spaceAbove > spaceBelow; const shouldFlipHorizontal = popoverWidth > spaceRight && spaceLeft > spaceRight; - // Apply flips based on space comparisons if (shouldFlipVertical && shouldFlipHorizontal) { selector = 'top-and-left'; @@ -411,6 +421,12 @@ window.mudpopoverHelper = { popoverContentNode.removeAttribute('data-mudpopover-flip'); } + if (isFlipOnOpen) { // store flip direction on open so it's not recalculated + if (!popoverContentNode.mudPopoverFliped) { + popoverContentNode.mudPopoverFliped = selector || 'none'; + } + } + // ensure the left is inside bounds if (left + offsetX < window.mudpopoverHelper.overflowPadding && // it's starting left of the screen Math.abs(left + offsetX) < selfRect.width) { // it's not starting so far left the entire box would be hidden @@ -436,17 +452,8 @@ window.mudpopoverHelper = { // adjust the popover position/maxheight if it contians a mud-list as it's first descendant // exceeds the bounds and doesn't have a max-height set by the user - // maxHeight adjustments stop the minute popoverNode is no longer inside the window - const firstChild = popoverContentNode.firstElementChild; - const list = firstChild && firstChild.classList.contains('mud-list') ? firstChild : null; + // maxHeight adjustments stop the minute popoverNode is no longer inside the window if (list) { - // Reset max-height if it was previously set and anchor is in bounds - if (popoverContentNode.mudHeight && anchorY > 0 && anchorY < window.innerHeight) { - popoverContentNode.style.maxHeight = null; - list.style.maxHeight = null; - popoverContentNode.mudHeight = null; - } - // Check if max-height is set on popover or list const hasMaxHeight = popoverContentNode.style.maxHeight != '' || list.style.maxHeight != ''; @@ -472,12 +479,6 @@ window.mudpopoverHelper = { } } } - - if (isFlipOnOpen) { // store flip direction on open so it's not recalculated - if (!popoverContentNode.mudPopoverFliped) { - popoverContentNode.mudPopoverFliped = selector || 'none'; - } - } } if (isPositionFixed) { @@ -640,7 +641,7 @@ class MudPopover { } createObservers(id) { - + console.log("createObservers"); // this is the origin of the popover in the dom, it can be nested inside another popover's content // e.g. the filter popover for datagrid, this would be the inside of where the mudpopover was placed // popoverNode.parentNode is it's immediate parent or the actual element in the above example @@ -650,10 +651,7 @@ class MudPopover { const popoverContentNode = document.getElementById('popovercontent-' + id); if (popoverNode && popoverNode.parentNode && popoverContentNode) { - // Add scroll event listeners to the content node and its parents up to the Body - const scrollableElements = window.mudpopoverHelper.popoverScrollListener(popoverNode); - - // add a resize observor to catch resize events + // add a resize observer to catch resize events const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const target = entry.target; @@ -667,6 +665,9 @@ class MudPopover { resizeObserver.observe(popoverNode.parentNode); + // Add scroll event listeners to the content node and its parents up to the Body + const scrollableElements = window.mudpopoverHelper.popoverScrollListener(popoverNode); + // Store all references needed for later cleanup this.map[id] = { popoverContentNode: popoverContentNode, @@ -680,6 +681,7 @@ class MudPopover { } disposeObservers(id) { + console.log("disposeObservers"); // Get references to items that need cleanup const { scrollableElements, parentResizeObserver } = this.map[id]; @@ -714,6 +716,7 @@ class MudPopover { } // create observers for this popover (resizeObserver and scroll Listeners) this.createObservers(id); + console.log("reposition popover for open"); // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); } @@ -754,7 +757,7 @@ class MudPopover { // data ticks is not 0 so let's reposition the popover and overlay if (tickAttribute > 0 && target.parentNode && this.map[id] && this.map[id].isOpened && target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { - + console.log("reposition popover for tick"); // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); @@ -914,10 +917,12 @@ class MudPopover { } window.mudpopoverHelper.debouncedResize = window.mudpopoverHelper.debounce(() => { + console.log("resize Popover"); window.mudpopoverHelper.placePopoverByClassSelector(); }, 25); -window.mudpopoverHelper.handleScroll = function() { +window.mudpopoverHelper.handleScroll = function () { + console.log("scroll popover"); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); }; From e2009e0169dfc2242c2c82a2712322da174f233c Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Tue, 8 Apr 2025 10:28:24 -0500 Subject: [PATCH 16/22] change from list to content node, remove extra logging, fix incorrect array assignment --- src/MudBlazor/TScripts/mudPopover.js | 83 ++++++++++++---------------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index d23766f23734..b1380dca2944 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -215,16 +215,10 @@ window.mudpopoverHelper = { popoverContentNode.style['min-width'] = (boundingRect.width) + 'px'; } - // reset heights and allow them to be changed after initial creation - const firstChild = popoverContentNode.firstElementChild; - const list = firstChild && firstChild.classList.contains('mud-list') ? firstChild : null; - if (list) { - // Reset max-height if it was previously set and anchor is in bounds - if (popoverContentNode.mudHeight && anchorY > 0 && anchorY < window.innerHeight) { - popoverContentNode.style.maxHeight = null; - list.style.maxHeight = null; - popoverContentNode.mudHeight = null; - } + // Reset max-height if it was previously set and anchor is in bounds + if (popoverContentNode.mudHeight && anchorY > 0 && anchorY < window.innerHeight) { + popoverContentNode.style.maxHeight = null; + popoverContentNode.mudHeight = null; } // get the top/left/ from popoverContentNode if the popover has been hardcoded for position @@ -450,33 +444,33 @@ window.mudpopoverHelper = { this.updatePopoverZIndex(popoverContentNode, appBarElements[0]); } - // adjust the popover position/maxheight if it contians a mud-list as it's first descendant + const firstChild = popoverContentNode.firstElementChild; + + // adjust the popover position/maxheight if it or firstChild does not have a max-height set (even if set to 'none') // exceeds the bounds and doesn't have a max-height set by the user - // maxHeight adjustments stop the minute popoverNode is no longer inside the window - if (list) { - // Check if max-height is set on popover or list - const hasMaxHeight = popoverContentNode.style.maxHeight != '' || list.style.maxHeight != ''; - - if (!hasMaxHeight) { - // in case of a reflow check if should show from top properly - let shouldShowFromTop = false; - // calculate list max height if it exceeds bounds - let listMaxHeight = window.innerHeight - top - offsetY - window.mudpopoverHelper.overflowPadding; // downwards - // moving upwards - if (top + offsetY < anchorY || top + offsetY == window.mudpopoverHelper.overflowPadding) { - shouldShowFromTop = true; - listMaxHeight = anchorY - window.mudpopoverHelper.overflowPadding; - } + // maxHeight adjustments stop the minute popoverNode is no longer inside the window + // Check if max-height is set on popover or firstChild + const hasMaxHeight = popoverContentNode.style.maxHeight != '' || (firstChild && firstChild.style.maxHeight != ''); + + if (!hasMaxHeight) { + // in case of a reflow check it should show from top properly + let shouldShowFromTop = false; + // calculate new max height if it exceeds bounds + let newMaxHeight = window.innerHeight - top - offsetY - window.mudpopoverHelper.overflowPadding; // downwards + // moving upwards + if (top + offsetY < anchorY || top + offsetY == window.mudpopoverHelper.overflowPadding) { + shouldShowFromTop = true; + newMaxHeight = anchorY - window.mudpopoverHelper.overflowPadding; + } - // if list calculated height exceeds the listmaxheight - if (list.offsetHeight > listMaxHeight) { - if (shouldShowFromTop) { // adjust top to show from top - top = window.mudpopoverHelper.overflowPadding; - offsetY = 0; - } - list.style.maxHeight = (listMaxHeight) + 'px'; - popoverContentNode.mudHeight = "setmaxheight"; + // if calculated height exceeds the new maxheight + if (popoverContentNode.offsetHeight > newMaxHeight) { + if (shouldShowFromTop) { // adjust top to show from top + top = window.mudpopoverHelper.overflowPadding; + offsetY = 0; } + popoverContentNode.style.maxHeight = (newMaxHeight) + 'px'; + popoverContentNode.mudHeight = "setmaxheight"; } } } @@ -641,7 +635,6 @@ class MudPopover { } createObservers(id) { - console.log("createObservers"); // this is the origin of the popover in the dom, it can be nested inside another popover's content // e.g. the filter popover for datagrid, this would be the inside of where the mudpopover was placed // popoverNode.parentNode is it's immediate parent or the actual element in the above example @@ -669,19 +662,15 @@ class MudPopover { const scrollableElements = window.mudpopoverHelper.popoverScrollListener(popoverNode); // Store all references needed for later cleanup - this.map[id] = { - popoverContentNode: popoverContentNode, - scrollableElements: scrollableElements, - parentResizeObserver: resizeObserver, - isOpened: false - }; + this.map[id].scrollableElements = scrollableElements; + this.map[id].parentResizeObserver = resizeObserver; + } else { console.warn(`Could not connect observers to popover with ID ${id}: One or more required elements not found`); } } disposeObservers(id) { - console.log("disposeObservers"); // Get references to items that need cleanup const { scrollableElements, parentResizeObserver } = this.map[id]; @@ -712,11 +701,11 @@ class MudPopover { if (target.classList.contains('mud-popover-open')) { // setup for an open popover and create observers if (this.map[id] && !this.map[id].isOpened) { - this.map[id].isOpened = true; + this.map[id].isOpened = true; } // create observers for this popover (resizeObserver and scroll Listeners) this.createObservers(id); - console.log("reposition popover for open"); + // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); } @@ -755,9 +744,9 @@ class MudPopover { // instead we use data-ticks since we know the newest data-ticks > 0 is the top most. const tickAttribute = target.getAttribute('data-ticks'); // data ticks is not 0 so let's reposition the popover and overlay + if (tickAttribute > 0 && target.parentNode && this.map[id] && this.map[id].isOpened && target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) { - console.log("reposition popover for tick"); // reposition popover individually window.mudpopoverHelper.placePopoverByNode(target); @@ -855,6 +844,8 @@ class MudPopover { // Store all references needed for later cleanup this.map[id] = { popoverContentNode: popoverContentNode, + scrollableElements: null, + parentResizeObserver: null, isOpened: false }; } @@ -917,12 +908,10 @@ class MudPopover { } window.mudpopoverHelper.debouncedResize = window.mudpopoverHelper.debounce(() => { - console.log("resize Popover"); window.mudpopoverHelper.placePopoverByClassSelector(); }, 25); window.mudpopoverHelper.handleScroll = function () { - console.log("scroll popover"); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-fixed'); window.mudpopoverHelper.placePopoverByClassSelector('mud-popover-overflow-flip-always'); }; From 5381e01c7793ac147e3dc61be60c0cd11e74110f Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Tue, 8 Apr 2025 11:45:45 -0500 Subject: [PATCH 17/22] adjust tests --- .../Components/DynamicTabsTests.cs | 10 +- .../Components/ToolTipTests.cs | 117 +++--------------- .../Services/Popover/PopoverServiceTests.cs | 7 +- 3 files changed, 25 insertions(+), 109 deletions(-) diff --git a/src/MudBlazor.UnitTests/Components/DynamicTabsTests.cs b/src/MudBlazor.UnitTests/Components/DynamicTabsTests.cs index a4f5b55d9c64..6069573b0287 100644 --- a/src/MudBlazor.UnitTests/Components/DynamicTabsTests.cs +++ b/src/MudBlazor.UnitTests/Components/DynamicTabsTests.cs @@ -104,12 +104,11 @@ public async Task BasicParameters_WithToolTips() actual.Should().BeEquivalentTo(expected); var parent = (IHtmlElement)item.Parent; - parent.Children.Should().HaveCount(1, because: "the button and no empty popover hint since it's not active"); + parent.Children.Should().HaveCount(2, because: "the button and the empty popover hint since it's not active"); await item.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); - var popover = comp.Find("div.mud-popover"); - var popoverId = popover.Id.Substring(15); + var popoverId = parent.Children[1].Id.Substring(8); var toolTip = comp.Find($"#popovercontent-{popoverId}"); @@ -134,12 +133,11 @@ public async Task BasicParameters_WithToolTips() actual.Should().BeEquivalentTo(expected); var parent = (IHtmlElement)item.Parent; - parent.Children.Should().HaveCount(1, because: "the button and no popover hint"); ; + parent.Children.Should().HaveCount(2, because: "the button and the empty popover hint"); ; await item.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); - var popover = comp.Find("div.mud-popover"); - var popoverId = popover.Id.Substring(15); + var popoverId = parent.Children[1].Id.Substring(8); var toolTip = comp.Find($"#popovercontent-{popoverId}"); diff --git a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs index fb112497bba3..f5c639d781e0 100644 --- a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs +++ b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs @@ -42,21 +42,23 @@ public async Task RenderContent(bool usingFocusout) button.ParentElement.ClassList.Should().Contain("mud-tooltip-root"); - //the button [0] and the popover node doesn't exist yet - button.ParentElement.Children.Should().HaveCount(1); + //the button [0] and [1] the popover npde + button.ParentElement.Children.Should().HaveCount(2); + + var popoverNode = button.ParentElement.Children[1]; + popoverNode.Id.Should().StartWith("popover-"); + + var popoverContentNode = comp.Find($"#popovercontent-{popoverNode.Id.Substring(8)}"); + + //no content for the popover node + popoverContentNode.Children.Should().BeEmpty(); //not visible by default tooltipComp.GetState(x => x.Visible).Should().BeFalse(); //trigger pointerover - await button.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); - //content should be visible - var popoverNode = button.ParentElement.Children[1]; - popoverNode.Id.Should().StartWith("popover-"); - - var popoverContentNode = comp.Find($"#popovercontent-{popoverNode.Id.Substring(8)}"); popoverContentNode.TextContent.Should().Be("my tooltip content text"); popoverContentNode.ClassList.Should().Contain("d-flex"); @@ -72,9 +74,7 @@ public async Task RenderContent(bool usingFocusout) button.ParentElement.FocusOut(); } //no content should be visible - comp.Markup.Should().NotContain("my tooltip content text"); - //the button [0] and the popover node doesn't exist again - button.ParentElement.Children.Should().HaveCount(1); + popoverContentNode.Children.Should().BeEmpty(); tooltipComp.GetState(x => x.Visible).Should().BeFalse(); } @@ -110,17 +110,20 @@ public async Task RenderTooltipFragment(bool usingFocusout) button.ParentElement.ClassList.Should().Contain("mud-tooltip-root"); //the button [0] and [1] the popover node - button.ParentElement.Children.Should().HaveCount(1); - - //trigger pointerover - - await button.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); + button.ParentElement.Children.Should().HaveCount(2); var popoverNode = button.ParentElement.Children[1]; popoverNode.Id.Should().StartWith("popover-"); var popoverContentNode = comp.Find($"#popovercontent-{popoverNode.Id.Substring(8)}"); + + //no content for the popover node + popoverContentNode.Children.Should().BeEmpty(); + + //trigger pointerover + await button.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); + //content should be visible popoverContentNode.ClassList.Should().Contain("mud-tooltip"); popoverContentNode.ClassList.Should().Contain("d-flex"); @@ -137,9 +140,7 @@ public async Task RenderTooltipFragment(bool usingFocusout) button.ParentElement.FocusOut(); } //no content should be visible - comp.Markup.Should().NotContain("My content"); - //the button [0] and the popover node doesn't exist again - button.ParentElement.Children.Should().HaveCount(1); + popoverContentNode.Children.Should().BeEmpty(); } [Test] @@ -356,83 +357,5 @@ public async Task Tooltip_Disabled_Button_OnPointerEnter_NoPopover() await button.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); comp.FindAll("div.mud-popover-open").Count.Should().Be(0); } - - [TestCase(0, 0)] - [TestCase(500, 500)] - [Test] - public void Tooltip_Debouncer_Initials(double duration, double delay) - { - var comp = Context.RenderComponent(p => - { - p.Add(x => x.Delay, delay); - p.Add(x => x.Duration, duration); - }); - - var tooltipComp = comp.FindComponent().Instance; - tooltipComp.Delay.Should().Be(delay); - tooltipComp.Duration.Should().Be(duration); - tooltipComp._previousDelay.Should().Be(delay); - tooltipComp._previousDuration.Should().Be(duration); - var button = comp.Find("button"); - button.Should().NotBeNull(); - } - - [TestCase(0, 0)] - [TestCase(500, 500)] - [Test] - public async Task Tooltip_Debouncer_Duration_and_Delay(double duration, double delay) - { - var comp = Context.RenderComponent(p => - { - p.Add(x => x.Delay, delay); - p.Add(x => x.Duration, duration); - }); - - var tooltipComp = comp.FindComponent().Instance; - - // cannot await or it waits until the debounce happens - var eventTask = tooltipComp.HandlePointerEnterAsync(); - if (delay > 0) - tooltipComp.ShowToolTip().Should().BeFalse(); - - await Task.Delay((int)delay + 50); - tooltipComp.ShowToolTip().Should().BeTrue(); - - await eventTask; // ensure all completed - - // cannot await or it waits until the debounce happens - eventTask = tooltipComp.HandlePointerLeaveAsync(); - if (duration > 0) - tooltipComp.ShowToolTip().Should().BeTrue(); - - await Task.Delay((int)(duration + delay) + 50); - tooltipComp.ShowToolTip().Should().BeFalse(); - - await eventTask; - } - - [TestCase(true)] - [TestCase(false)] - [Test] - public async Task Tooltip_ShowOnHover(bool showOnHover) - { - var comp = Context.RenderComponent(p => - { - p.Add(x => x.ShowOnHover, showOnHover); - }); - // we don't need to await Task.Delay to account for Delay/Duration since the await Handle takes care of it. - var tooltipComp = comp.FindComponent().Instance; - tooltipComp.ShowOnHover.Should().Be(showOnHover); - if (!showOnHover) - { - await tooltipComp.HandlePointerEnterAsync(); - tooltipComp.ShowToolTip().Should().BeFalse(); - } - else - { - await tooltipComp.HandlePointerEnterAsync(); - tooltipComp.ShowToolTip().Should().BeTrue(); - } - } } } diff --git a/src/MudBlazor.UnitTests/Services/Popover/PopoverServiceTests.cs b/src/MudBlazor.UnitTests/Services/Popover/PopoverServiceTests.cs index fe3d6f0ac746..f8ab51148fc1 100644 --- a/src/MudBlazor.UnitTests/Services/Popover/PopoverServiceTests.cs +++ b/src/MudBlazor.UnitTests/Services/Popover/PopoverServiceTests.cs @@ -2,11 +2,6 @@ // MudBlazor licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using FluentAssertions; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging.Abstractions; @@ -545,7 +540,7 @@ public async Task CreatePopoverAsync_UpdatePopoverAsync_DestroyPopoverAsync_Shou .Callback(signalEvent.Set); jsRuntimeMock.Setup(x => x.InvokeAsync("mudPopover.initialize", It.IsAny(), - It.Is(y => y.Length == 2))) + It.Is(y => y.Length == 3))) .ReturnsAsync(Mock.Of()) .Verifiable(); From 51c292a52d1dfbdd5705f44582f2e5422526725b Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Wed, 9 Apr 2025 10:02:55 -0500 Subject: [PATCH 18/22] added unit test for partials --- .../Components/ToolTipTests.cs | 36 +++++++++++++++++++ .../Components/Tooltip/MudTooltip.razor.cs | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs index f5c639d781e0..96875bb4ac41 100644 --- a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs +++ b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs @@ -357,5 +357,41 @@ public async Task Tooltip_Disabled_Button_OnPointerEnter_NoPopover() await button.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); comp.FindAll("div.mud-popover-open").Count.Should().Be(0); } + + [TestCase(true)] + [TestCase(false)] + [Test] + public async Task Tooltip_Handle_Pointer_Events(bool showOnHover) + { + var comp = Context.RenderComponent(parameters => parameters + .Add(x => x.ShowOnHover, showOnHover) + .Add(x => x.ShowOnClick, true) + .Add(x => x.Text, "tooltip text") + ); + + var div = comp.Find(".mud-tooltip-root"); + div.Should().NotBeNull(); + + var tooltip = comp.Instance; + tooltip.Should().NotBeNull(); + + await tooltip.HandlePointerEnterAsync(); + tooltip._visibleState.Value.Should().Be(showOnHover); + + if (showOnHover) + { + await tooltip.HandlePointerLeaveAsync(); + tooltip._visibleState.Value.Should().Be(!showOnHover); + } + + await div.PointerEnterAsync(new PointerEventArgs()); + tooltip._visibleState.Value.Should().Be(showOnHover); + + if (showOnHover) + { + await div.PointerLeaveAsync(new PointerEventArgs()); + tooltip._visibleState.Value.Should().Be(!showOnHover); + } + } } } diff --git a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs index d71defc4e02b..a71af85f8df8 100644 --- a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs +++ b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs @@ -7,7 +7,7 @@ namespace MudBlazor #nullable enable public partial class MudTooltip : MudComponentBase { - private readonly ParameterState _visibleState; + internal readonly ParameterState _visibleState; private Origin _anchorOrigin; private Origin _transformOrigin; public MudTooltip() From cc146bf7dc39bd6c1a104874e1eb34e586c5fa4c Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Wed, 9 Apr 2025 13:47:19 -0500 Subject: [PATCH 19/22] initial review changes --- .../Popover/PopoverFlipDirectionTest.razor | 169 +++++++++--------- .../Tooltip/TooltipNotRemovedTest.razor | 9 +- .../Components/ToolTipTests.cs | 20 +-- 3 files changed, 100 insertions(+), 98 deletions(-) diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor index 27799fcdec79..a9d2c1142427 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor @@ -1,97 +1,100 @@  - @(_expanded ? "Collapse" : "Expand") - @(_expanded ? "Collapse" : "Expand") for 5 s - Expand list in 5 s - - -
- - - @(_expanded ? "Collapse" : "Expand") + @(_expanded ? "Collapse" : "Expand") for 5 s + Expand list in 5 s + + +
+ + + - Top Left - Top Center - Top Right - Bottom Left - Bottom Center - Bottom Right - Center Left - Center Center - Center Right - - - Top Left - Top Center - Top Right - Bottom Left - Bottom Center - Bottom Right - Center Left - Center Center - Center Right - -
-
- - - - - - - - - - @if (_expandList) - { - - - - - - - - - - - - - - - - - - - } - - @if (!_hideBottom) - { - @(_expanded ? "Collapse" : "Expand") - @(_expanded ? "Collapse" : "Expand") for 5 s - - -
- -
-
- } + Top Left + Top Center + Top Right + Bottom Left + Bottom Center + Bottom Right + Center Left + Center Center + Center Right +
+ + Top Left + Top Center + Top Right + Bottom Left + Bottom Center + Bottom Right + Center Left + Center Center + Center Right + +
+
+ + + + + + + + + + @if (_expandList) + { + + + + + + + + + + + + + + + + + + + } + + @if (!_hideBottom) + { + @(_expanded ? "Collapse" : "Expand") + @(_expanded ? "Collapse" : "Expand") for 5 s + + +
+ +
+
+ }
@code { - public static string __description__ = "Popover should take the best available space"; - private Int32 _height { get; set; } = 600; - private Int32 _heightBottom { get; set; } = 400; - private Boolean _hideBottom { get; set; } - private Boolean _expandList { get; set; } + + + public static string __description__ = "Popover should take the best available space"; + + private Int32 _height = 600; + private Int32 _heightBottom = 400; + private bool _hideBottom; + private bool _expandList; private Origin _anchor = Origin.BottomLeft; private Origin _transform = Origin.TopLeft; private bool _expanded = true; - private DropdownSettings _dropdownSettings => new DropdownSettings + private readonly DropdownSettings _dropdownSettings = new DropdownSettings { OverflowBehavior = OverflowBehavior.FlipAlways }; diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor index 2c39811d9cce..9e940b32b4ef 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor @@ -1,11 +1,10 @@  - + @for (var i = 0; i < 200; i++) { -
- BTN @i + BTN @i
} @@ -13,7 +12,7 @@ @code { - private Guid key; + private Guid _key; protected override Task OnInitializedAsync() { @@ -25,7 +24,7 @@ { while (true) { - this.key = Guid.NewGuid(); + this._key = Guid.NewGuid(); InvokeAsync(StateHasChanged).CatchAndLog(); await Task.Delay(100); } diff --git a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs index 96875bb4ac41..8a48dacff932 100644 --- a/src/MudBlazor.UnitTests/Components/ToolTipTests.cs +++ b/src/MudBlazor.UnitTests/Components/ToolTipTests.cs @@ -48,10 +48,10 @@ public async Task RenderContent(bool usingFocusout) var popoverNode = button.ParentElement.Children[1]; popoverNode.Id.Should().StartWith("popover-"); - var popoverContentNode = comp.Find($"#popovercontent-{popoverNode.Id.Substring(8)}"); + var popoverContentNode = () => comp.Find($"#popovercontent-{popoverNode.Id.Substring(8)}"); //no content for the popover node - popoverContentNode.Children.Should().BeEmpty(); + popoverContentNode().Children.Should().BeEmpty(); //not visible by default tooltipComp.GetState(x => x.Visible).Should().BeFalse(); @@ -59,8 +59,8 @@ public async Task RenderContent(bool usingFocusout) //trigger pointerover await button.ParentElement.TriggerEventAsync("onpointerenter", new PointerEventArgs()); - popoverContentNode.TextContent.Should().Be("my tooltip content text"); - popoverContentNode.ClassList.Should().Contain("d-flex"); + popoverContentNode().TextContent.Should().Be("my tooltip content text"); + popoverContentNode().ClassList.Should().Contain("d-flex"); tooltipComp.GetState(x => x.Visible).Should().BeTrue(); @@ -74,7 +74,7 @@ public async Task RenderContent(bool usingFocusout) button.ParentElement.FocusOut(); } //no content should be visible - popoverContentNode.Children.Should().BeEmpty(); + popoverContentNode().Children.Should().BeEmpty(); tooltipComp.GetState(x => x.Visible).Should().BeFalse(); } @@ -358,9 +358,9 @@ public async Task Tooltip_Disabled_Button_OnPointerEnter_NoPopover() comp.FindAll("div.mud-popover-open").Count.Should().Be(0); } + [Test] [TestCase(true)] [TestCase(false)] - [Test] public async Task Tooltip_Handle_Pointer_Events(bool showOnHover) { var comp = Context.RenderComponent(parameters => parameters @@ -376,21 +376,21 @@ public async Task Tooltip_Handle_Pointer_Events(bool showOnHover) tooltip.Should().NotBeNull(); await tooltip.HandlePointerEnterAsync(); - tooltip._visibleState.Value.Should().Be(showOnHover); + tooltip.GetState(x => x.Visible).Should().Be(showOnHover); if (showOnHover) { await tooltip.HandlePointerLeaveAsync(); - tooltip._visibleState.Value.Should().Be(!showOnHover); + tooltip.GetState(x => x.Visible).Should().Be(!showOnHover); } await div.PointerEnterAsync(new PointerEventArgs()); - tooltip._visibleState.Value.Should().Be(showOnHover); + tooltip.GetState(x => x.Visible).Should().Be(showOnHover); if (showOnHover) { await div.PointerLeaveAsync(new PointerEventArgs()); - tooltip._visibleState.Value.Should().Be(!showOnHover); + tooltip.GetState(x => x.Visible).Should().Be(!showOnHover); } } } From a0ab49fd0f8e3a2a1561a2b5ffaeadab20225fc8 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Fri, 11 Apr 2025 13:37:08 -0500 Subject: [PATCH 20/22] regression fix for mudmenu, includes a new viewer case --- .../Menu/MenuEdgeCasesTest.razor | 179 ++++++++++++++++++ src/MudBlazor/Components/Menu/MudMenu.razor | 3 +- .../Components/Menu/MudMenu.razor.cs | 1 + src/MudBlazor/TScripts/mudPopover.js | 4 +- 4 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor new file mode 100644 index 000000000000..032dae706ff7 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor @@ -0,0 +1,179 @@ + + + Test Application + + + @{ + var priority = "1"; + switch (priority) + { + case "1": + Priority 1 + break; + case "2": + Priority 2 + break; + case "3": + Priority 3 + break; + case "4": + Priority 4 + break; + case "5": + Priority 5 + break; + case null: + None + break; + } + } + + +
+ Priority 1 + Priority 2 + Priority 3 + Priority 4 + Priority 5 + None +
+
+
+ + + @{ + var priority = "1"; + switch (priority) + { + case "1": + Priority 1 + break; + case "2": + Priority 2 + break; + case "3": + Priority 3 + break; + case "4": + Priority 4 + break; + case "5": + Priority 5 + break; + case null: + None + break; + } + } + + +
+ Priority 1 + Priority 2 + Priority 3 + Priority 4 + Priority 5 + None +
+
+
+
+ + @* On initial load these don't respect the mouseover leave event only activating and not deactivating *@ + + @foreach (var menu in menuList) + { + + + @($"Menu {menu}") + + + + + + + + + + } + + +
+ Click + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + @{ + var priority = "1"; + switch (priority) + { + case "1": + Priority 1 + break; + case "2": + Priority 2 + break; + case "3": + Priority 3 + break; + case "4": + Priority 4 + break; + case "5": + Priority 5 + break; + case null: + None + break; + } + } + + +
+ Priority 1 + Priority 2 + Priority 3 + Priority 4 + Priority 5 + None +
+
+
+
+
+ +@code { + public static string __description__ = "4 menu edge cases for visual testing, initial load, in appbar, nested, and in dialog."; + private readonly string[] menuList = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]; + private bool _visible = false; +} diff --git a/src/MudBlazor/Components/Menu/MudMenu.razor b/src/MudBlazor/Components/Menu/MudMenu.razor index 6434b4560ae9..f8d45439581a 100644 --- a/src/MudBlazor/Components/Menu/MudMenu.razor +++ b/src/MudBlazor/Components/Menu/MudMenu.razor @@ -92,8 +92,7 @@ @if (ParentMenu is null) { - new CssBuilder() .AddClass(PopoverClass) + .AddClass("mud-popover-nested", ParentMenu is not null) .AddClass("mud-popover-position-override", PositionAtCursor) .Build(); diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index b1380dca2944..adf45a6fcfad 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -766,7 +766,9 @@ class MudPopover { } } - if (highestTickItem) { + const isNested = highestTickItem.classList.contains('mud-popover-nested'); + console.log('isNested: ', isNested); + if (highestTickItem && !isNested) { window.mudpopoverHelper.updatePopoverOverlay(highestTickItem); } } From 86af6b9e52eedac477290159d33d1fa2b7c18749 Mon Sep 17 00:00:00 2001 From: Versile Johnson II Date: Fri, 11 Apr 2025 14:29:20 -0500 Subject: [PATCH 21/22] removed extra console.log --- src/MudBlazor/TScripts/mudPopover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js index adf45a6fcfad..d3d2dc705e6b 100644 --- a/src/MudBlazor/TScripts/mudPopover.js +++ b/src/MudBlazor/TScripts/mudPopover.js @@ -767,7 +767,7 @@ class MudPopover { } const isNested = highestTickItem.classList.contains('mud-popover-nested'); - console.log('isNested: ', isNested); + if (highestTickItem && !isNested) { window.mudpopoverHelper.updatePopoverOverlay(highestTickItem); } From 692329cb313599ca4d52ef1e95a420b1e1030ce4 Mon Sep 17 00:00:00 2001 From: ScarletKuro Date: Sun, 20 Apr 2025 00:47:48 +0300 Subject: [PATCH 22/22] nit --- .../Menu/MenuEdgeCasesTest.razor | 6 +-- .../Popover/PopoverFlipDirectionTest.razor | 44 ++++++++----------- .../Tooltip/TooltipNotRemovedTest.razor | 19 ++++---- .../Components/Tooltip/MudTooltip.razor.cs | 2 +- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor index 032dae706ff7..174ec0d05568 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Menu/MenuEdgeCasesTest.razor @@ -81,7 +81,7 @@ @* On initial load these don't respect the mouseover leave event only activating and not deactivating *@ - @foreach (var menu in menuList) + @foreach (var menu in _menuList) { @@ -174,6 +174,6 @@ @code { public static string __description__ = "4 menu edge cases for visual testing, initial load, in appbar, nested, and in dialog."; - private readonly string[] menuList = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]; - private bool _visible = false; + private readonly string[] _menuList = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]; + private bool _visible; } diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor index a9d2c1142427..d4efc91780ba 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Popover/PopoverFlipDirectionTest.razor @@ -1,13 +1,13 @@  @(_expanded ? "Collapse" : "Expand") - @(_expanded ? "Collapse" : "Expand") for 5 s - Expand list in 5 s + @(_expanded ? "Collapse" : "Expand") for 5 s + Expand list in 5 s
- - + + @@ -71,48 +71,42 @@ @if (!_hideBottom) { @(_expanded ? "Collapse" : "Expand") - @(_expanded ? "Collapse" : "Expand") for 5 s + @(_expanded ? "Collapse" : "Expand") for 5 s
- +
} @code { - - - public static string __description__ = "Popover should take the best available space"; - private Int32 _height = 600; - private Int32 _heightBottom = 400; private bool _hideBottom; private bool _expandList; + private bool _expanded = true; + private int _height = 600; + private int _heightBottom = 400; private Origin _anchor = Origin.BottomLeft; private Origin _transform = Origin.TopLeft; - private bool _expanded = true; - private readonly DropdownSettings _dropdownSettings = new DropdownSettings - { + private readonly DropdownSettings _dropdownSettings = new() + { OverflowBehavior = OverflowBehavior.FlipAlways }; - private void OnExpandCollapseClick() - { - _expanded = !_expanded; - } + private void OnExpandCollapseClick() => _expanded = !_expanded; - private async Task OnExpandCollapseFor5sClick() + private async Task OnExpandCollapseFor5sClickAsync() { - var cur = _expanded; - _expanded = !cur; - await Task.Delay(5000); - _expanded = cur; - } + var expanded = _expanded; + _expanded = !expanded; + await Task.Delay(5000); + _expanded = expanded; + } - private async Task OnExpandListIn5s() + private async Task OnExpandListIn5sAsync() { _expandList = false; await Task.Delay(5000); diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor index 9e940b32b4ef..a675c8aaf52a 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Tooltip/TooltipNotRemovedTest.razor @@ -1,10 +1,12 @@ - - + @for (var i = 0; i < 200; i++) {
+ @{ + var current = i; + } - BTN @i + BTN @current
} @@ -16,17 +18,18 @@ protected override Task OnInitializedAsync() { - RunThing().CatchAndLog(); + var cts = new CancellationTokenSource(); + RunThingAsync(cts.Token).CatchAndLog(); return base.OnInitializedAsync(); } - private async Task RunThing() + private async Task RunThingAsync(CancellationToken cancellationToken) { - while (true) + while (!cancellationToken.IsCancellationRequested) { - this._key = Guid.NewGuid(); + _key = Guid.NewGuid(); InvokeAsync(StateHasChanged).CatchAndLog(); - await Task.Delay(100); + await Task.Delay(100, cancellationToken); } } } diff --git a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs index a71af85f8df8..d71defc4e02b 100644 --- a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs +++ b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs @@ -7,7 +7,7 @@ namespace MudBlazor #nullable enable public partial class MudTooltip : MudComponentBase { - internal readonly ParameterState _visibleState; + private readonly ParameterState _visibleState; private Origin _anchorOrigin; private Origin _transformOrigin; public MudTooltip()