@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()
{
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 aeb767338f7e..52a8fdfb5754 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.
@@ -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;
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 318c46e43e0c..d3d2dc705e6b 100644
--- a/src/MudBlazor/TScripts/mudPopover.js
+++ b/src/MudBlazor/TScripts/mudPopover.js
@@ -3,7 +3,12 @@
// See the LICENSE file in the project root for more information.
window.mudpopoverHelper = {
+ // set by the class MudPopover in initialize
+ mainContainerClass: null,
+ overflowPadding: 24,
+ flipMargin: 0,
+ // used for setting a debounce
debounce: function (func, wait) {
let timeout;
return function executedFunction(...args) {
@@ -16,19 +21,54 @@ window.mudpopoverHelper = {
};
},
- rafThrottle: function (func) {
- let ticking = false;
- return function (...args) {
- if (!ticking) {
- window.requestAnimationFrame(() => {
- func.apply(this, args);
- ticking = false;
- });
- ticking = true;
- }
- };
+ basePopoverZIndex: parseInt(getComputedStyle(document.documentElement)
+ .getPropertyValue('--mud-zindex-popover')) || 1200,
+
+ baseTooltipZIndex: parseInt(getComputedStyle(document.documentElement)
+ .getPropertyValue('--mud-zindex-tooltip')) || 1600,
+
+ // static set of replacement values
+ 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',
+ },
+
},
+ // used to calculate the position of the popover
calculatePopoverPosition: function (list, boundingRect, selfRect) {
let top = 0;
let left = 0;
@@ -100,74 +140,30 @@ window.mudpopoverHelper = {
}
return {
- top: top, left: left, offsetX: offsetX, offsetY: offsetY
+ top: top, left: left, offsetX: offsetX, offsetY: offsetY, anchorY: top, anchorX: left
};
},
- 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,
-
+ // 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);
}
}
-
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
@@ -176,41 +172,57 @@ window.mudpopoverHelper = {
const id = popoverNode.id.substr(8);
const popoverContentNode = document.getElementById('popovercontent-' + id);
- if (!popoverContentNode) {
- return;
- }
+ // if the popover doesn't exist we stop
+ if (!popoverContentNode) return;
+
const classList = popoverContentNode.classList;
- if (classList.contains('mud-popover-open') == false) {
- return;
- }
+ // if the popover isn't open we stop
+ if (!classList.contains('mud-popover-open')) return;
- if (classSelector) {
- if (classList.contains(classSelector) == false) {
- return;
- }
- }
+ // if a classSelector was supplied and doesn't exist we stop
+ if (classSelector && !classList.contains(classSelector)) return;
+
+ // Batch DOM reads
let boundingRect = popoverNode.parentNode.getBoundingClientRect();
- // allow them to be changed after initial creation
+ 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';
+ const classListArray = Array.from(classList);
+
+ // calculate position based on opening anchor/transform
+ 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
+ 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 (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);
+ // 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;
+ }
- 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')) {
+ if (isPositionOverride) {
left = parseInt(popoverContentNode.style['left']) || left;
top = parseInt(popoverContentNode.style['top']) || top;
// no offset when hardcoded
@@ -227,7 +239,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;
@@ -235,124 +247,235 @@ 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);
-
+ // 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) {
+ const popoverHeight = popoverContentNode.offsetHeight;
+ const popoverWidth = popoverContentNode.offsetWidth;
+ // For mud-popover-top-left
+
if (classList.contains('mud-popover-top-left')) {
- if (deltaBottom < graceMargin && deltaToRight < graceMargin && spaceToTop >= selfRect.height && deltaToLeft >= selfRect.width) {
+ // Space available in current direction
+ 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 = 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 = popoverHeight > spaceBelow && spaceAbove > spaceBelow;
+ const shouldFlipHorizontal = popoverWidth > spaceRight && spaceLeft > spaceRight;
+ // Apply flips based on space comparisons
+ if (shouldFlipVertical && shouldFlipHorizontal) {
selector = 'top-and-left';
- } else if (deltaBottom < graceMargin && spaceToTop >= selfRect.height) {
+ }
+ else if (shouldFlipVertical) {
selector = 'top';
- } else if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width) {
+ }
+ else if (shouldFlipHorizontal) {
selector = 'left';
}
- } else if (classList.contains('mud-popover-top-center')) {
- if (deltaBottom < graceMargin && spaceToTop >= selfRect.height) {
+ }
+
+ // 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 - 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 (popoverHeight > spaceBelow && spaceAbove > spaceBelow) {
selector = 'top';
}
- } else if (classList.contains('mud-popover-top-right')) {
- if (deltaBottom < graceMargin && deltaToLeft < graceMargin && spaceToTop >= selfRect.height && deltaToRight >= selfRect.width) {
+ }
+
+ // For mud-popover-top-right
+ else if (classList.contains('mud-popover-top-right')) {
+ // Space available in current direction
+ const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin;
+ const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin;
+
+ // Space available in opposite direction
+ 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 = popoverHeight > spaceBelow && spaceAbove > spaceBelow;
+ const shouldFlipHorizontal = popoverWidth > spaceLeft && spaceRight > spaceLeft;
+
+ if (shouldFlipVertical && shouldFlipHorizontal) {
selector = 'top-and-right';
- } else if (deltaBottom < graceMargin && spaceToTop >= selfRect.height) {
+ }
+ else if (shouldFlipVertical) {
selector = 'top';
- } else if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width) {
+ }
+ else if (shouldFlipHorizontal) {
selector = 'right';
}
}
+ // For mud-popover-center-left
else if (classList.contains('mud-popover-center-left')) {
- if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width) {
+ // Space available in current vs opposite direction
+ const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin;
+ const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin;
+
+ if (popoverWidth > spaceRight && spaceLeft > spaceRight) {
selector = 'left';
}
}
+
+ // For mud-popover-center-right
else if (classList.contains('mud-popover-center-right')) {
- if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width) {
+ // Space available in current vs opposite direction
+ const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin;
+ const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin;
+
+ if (popoverWidth > spaceLeft && spaceRight > spaceLeft) {
selector = 'right';
}
}
+
+ // For mud-popover-bottom-left
else if (classList.contains('mud-popover-bottom-left')) {
- if (deltaTop < graceMargin && deltaToRight < graceMargin && deltaBottom >= 0 && deltaToLeft >= selfRect.width) {
+ // Space available in current direction
+ const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin;
+ const spaceRight = window.innerWidth - anchorX - window.mudpopoverHelper.flipMargin;
+
+ // Space available in opposite direction
+ 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 = popoverHeight > spaceAbove && spaceBelow > spaceAbove;
+ const shouldFlipHorizontal = popoverWidth > spaceRight && spaceLeft > spaceRight;
+
+ if (shouldFlipVertical && shouldFlipHorizontal) {
selector = 'bottom-and-left';
- } else if (deltaTop < graceMargin && deltaBottom >= 0) {
+ }
+ else if (shouldFlipVertical) {
selector = 'bottom';
- } else if (deltaToRight < graceMargin && deltaToLeft >= selfRect.width) {
+ }
+ else if (shouldFlipHorizontal) {
selector = 'left';
}
- } else if (classList.contains('mud-popover-bottom-center')) {
- if (deltaTop < graceMargin && deltaBottom >= 0) {
+ }
+
+ // For mud-popover-bottom-center
+ else if (classList.contains('mud-popover-bottom-center')) {
+ // Space available in current vs opposite direction
+ const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin;
+ const spaceBelow = window.innerHeight - anchorY - window.mudpopoverHelper.flipMargin;
+
+ if (popoverHeight > spaceAbove && spaceBelow > spaceAbove) {
selector = 'bottom';
}
- } else if (classList.contains('mud-popover-bottom-right')) {
- if (deltaTop < graceMargin && deltaToLeft < graceMargin && deltaBottom >= 0 && deltaToRight >= selfRect.width) {
+ }
+
+ // For mud-popover-bottom-right
+ else if (classList.contains('mud-popover-bottom-right')) {
+ // Space available in current direction
+ const spaceAbove = anchorY - window.mudpopoverHelper.flipMargin;
+ const spaceLeft = anchorX - window.mudpopoverHelper.flipMargin;
+
+ // Space available in opposite direction
+ 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 = popoverHeight > spaceAbove && spaceBelow > spaceAbove;
+ const shouldFlipHorizontal = popoverWidth > spaceLeft && spaceRight > spaceLeft;
+
+ if (shouldFlipVertical && shouldFlipHorizontal) {
selector = 'bottom-and-right';
- } else if (deltaTop < graceMargin && deltaBottom >= 0) {
+ }
+ else if (shouldFlipVertical) {
selector = 'bottom';
- } else if (deltaToLeft < graceMargin && deltaToRight >= selfRect.width) {
+ }
+ else if (shouldFlipHorizontal) {
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;
- popoverContentNode.setAttribute('data-mudpopover-flip', 'flipped');
+ popoverContentNode.setAttribute('data-mudpopover-flip', selector);
}
else {
- // did not flip, ensure the left and top are inside bounds
- // appbaroffset is another section
- 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');
+ }
- // will be covered by appbar so adjust zindex with appbar as parent
- if (top + offsetY < appBarOffset &&
- appBarElements.length > 0) {
- this.updatePopoverZIndex(popoverContentNode, appBarElements[0]);
- //console.log(`top: ${top} | offsetY: ${offsetY} | total: ${top + offsetY} | appBarOffset: ${appBarOffset}`);
+ if (isFlipOnOpen) { // store flip direction on open so it's not recalculated
+ if (!popoverContentNode.mudPopoverFliped) {
+ popoverContentNode.mudPopoverFliped = selector || 'none';
}
+ }
- 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;
- }
+ // 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
+ left = window.mudpopoverHelper.overflowPadding;
+ // set offsetX to 0 to avoid double offset
+ offsetX = 0;
+ }
- // 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';
- }
- popoverContentNode.removeAttribute('data-mudpopover-flip');
+ // ensure the top is inside bounds
+ 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 = window.mudpopoverHelper.overflowPadding;
+ // set offsetY to 0 to avoid double offset
+ offsetY = 0;
}
- if (classList.contains('mud-popover-overflow-flip-onopen')) {
- if (!popoverContentNode.mudPopoverFliped) {
- popoverContentNode.mudPopoverFliped = selector || 'none';
+ // will be covered by appbar so adjust zindex with appbar as parent
+ if (top + offsetY < appBarOffset &&
+ appBarElements.length > 0) {
+ this.updatePopoverZIndex(popoverContentNode, appBarElements[0]);
+ }
+
+ 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
+ // 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 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";
}
}
}
- if (window.getComputedStyle(popoverNode).position == 'fixed') {
+ if (isPositionFixed) {
popoverContentNode.style['position'] = 'fixed';
}
else if (!classList.contains('mud-popover-fixed')) {
@@ -360,7 +483,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;
@@ -371,11 +494,10 @@ 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 (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;
}
}
@@ -384,55 +506,35 @@ 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;
- }
- },
-
+ // 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(".mud-popover-provider").length;
+ 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")) {
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
@@ -446,6 +548,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');
@@ -501,6 +604,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', window.mudpopoverHelper.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 {
@@ -508,210 +632,290 @@ class MudPopover {
constructor() {
this.map = {};
this.contentObserver = null;
- this.mainContainerClass = 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');
- }
+ 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);
- window.mudpopoverHelper.placePopoverByNode(target);
- }
- 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;
- }
- }
- }
+ // 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);
- // 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;
- }
+ if (popoverNode && popoverNode.parentNode && popoverContentNode) {
+ // add a resize observer 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();
}
}
+ }
+ });
- if (highestTickItem) {
- window.mudpopoverHelper.updatePopoverOverlay(highestTickItem);
- }
+ resizeObserver.observe(popoverNode.parentNode);
- if (tickValues.length == 0) {
- continue;
- }
+ // Add scroll event listeners to the content node and its parents up to the Body
+ const scrollableElements = window.mudpopoverHelper.popoverScrollListener(popoverNode);
- 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;
- }
+ // Store all references needed for later cleanup
+ 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) {
+ // Get references to items that need cleanup
+ const { scrollableElements, parentResizeObserver } = this.map[id];
- if (childNode.skipZIndex == true) {
- continue;
+ // 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.handleScroll);
+ }
+ });
+ }
+
+ // 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;
+ 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;
+ }
+ // 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
+ if (this.map[id] && this.map[id].isOpened) {
+ this.map[id].isOpened = false;
+ }
+ // wait this long until we "move it off screen"
+ const delay = parseFloat(target.style['transition-duration']) || 0;
+ if (delay == 0) {
+ // remove left and top styles
+ target.style.removeProperty('left');
+ target.style.removeProperty('top');
+ }
+ 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');
+ }
+ }, delay);
+ }
+ // 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') {
+ // 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
+
+ if (tickAttribute > 0 && target.parentNode && this.map[id] && this.map[id].isOpened &&
+ target.parentNode.classList.contains(window.mudpopoverHelper.mainContainerClass)) {
+ // reposition popover individually
+ window.mudpopoverHelper.placePopoverByNode(target);
+
+ // 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;
+
+ // 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 (tickValue > highestTickValue) {
+ highestTickValue = tickValue;
+ highestTickItem = child;
}
- const newIndex = window.mudpopoverHelper.basePopoverZIndex + sortedTickValues.indexOf(tickValue) + 3;
- childNode.style['z-index'] = newIndex;
}
}
+
+ const isNested = highestTickItem.classList.contains('mud-popover-nested');
+
+ if (highestTickItem && !isNested) {
+ 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) {
+ console.error(`No Popover Container found with class ${containerClass}`);
return;
}
+ // store options from PopoverOptions in mudpopoverHelper
+ window.mudpopoverHelper.mainContainerClass = containerClass;
+ window.mudpopoverHelper.overflowPadding = overflowPadding;
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: ['data-ticks','class'] // limit to just data-ticks and class changes
+ };
- this.mainContainerClass = containerClass;
+ // Dispose of any existing observer before creating a new one
+ if (this.contentObserver) {
+ this.contentObserver.disconnect();
+ this.contentObserver = null;
+ }
- 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 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);
+ }
}
+ });
- this.contentObserver = new ResizeObserver(entries => {
- window.mudpopoverHelper.placePopoverByClassSelector();
- });
+ observer.observe(provider, config);
+ // store it so we can dispose of it properly
+ this.contentObserver = observer;
- this.contentObserver.observe(mainContent[0]);
- }
+ // setup event listeners
+ window.addEventListener('resize', window.mudpopoverHelper.debouncedResize, { passive: true });
+ window.addEventListener('scroll', window.mudpopoverHelper.handleScroll, { 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.initialize(this.mainContainerClass);
+ // this happens when a popover is created in the dom (not necessarily displayed)
+ // 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
+ // popoverNode.parentNode is it's immediate parent or the actual | element in the above example
const popoverNode = document.getElementById('popover-' + id);
- mudpopoverHelper.popoverScrollListener(popoverNode);
- const popoverContentNode = document.getElementById('popovercontent-' + id);
- 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);
-
- 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);
- }
- }
- }
- });
- 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);
+ // 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);
- this.map[id] = {
- popoverContentNode: popoverContentNode,
- mutationObserver: observer,
- resizeObserver: resizeObserver,
- contentNodeObserver: contentNodeObserver
- };
- }
+ // Store all references needed for later cleanup
+ this.map[id] = {
+ popoverContentNode: popoverContentNode,
+ scrollableElements: null,
+ parentResizeObserver: null,
+ 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]) {
+ if (!this.map[id]) {
+ return; // Nothing to disconnect
+ }
+
+ try {
+ // 1. Remove individual observers and listeners that might exist
+ this.disposeObservers(id);
- const item = this.map[id]
- item.mutationObserver.disconnect();
- item.resizeObserver.disconnect();
- item.contentNodeObserver.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) {
- 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.handleScroll);
+ } 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();
-}, 100);
+}, 25);
-const throttledScroll = window.mudpopoverHelper.rafThrottle(() => {
+window.mudpopoverHelper.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.mudPopover = new MudPopover();
\ No newline at end of file
|