I am attempting to re-create a design system in SCSS/JavaScript that has the following requirements:
After studying a multitude of tutorials and attempting various approaches I am about 99% complete, apart from a curious issue with Chrome (Firefox works perfectly).
In my sticky table cell, when the cursor reaches a row that contains wrapped text, the menu suddenly appears in the wrong place (always 17 pixels too far right). Stranger still, on the first hover it appears correctly but then subsequently in the wrong place, as are all the following row menus:
I believe the issue is JavaScript-related rather than CSS, as the console appears to produce different values for the menu button's left
position (shown in video).
https://codepen.io/evildr/pen/LYvxBMJ
I'd be very grateful if anyone can tell me what is actually causing the discrepancy to occur, and why Chrome/Firefox behave differently?
function WireUpKebabMenusOnMouseOver() {
// Select all elements with class 'MenuContainer'
var menuContainers = document.querySelectorAll('.MenuContainer');
// Remove any existing mouseover event listeners
menuContainers.forEach(function (menuContainer) {
var menuButton = menuContainer.querySelector('button');
var wrapperDiv = menuContainer.querySelector('.wrapper');
if (menuButton) {
menuButton.removeEventListener('mouseover', handleMouseOver);
menuButton.removeEventListener('mouseleave', handleMouseLeave);
menuButton.addEventListener('mouseover', handleMouseOver);
menuButton.addEventListener('mouseleave', function (event) {
handleMouseLeave(event, menuContainer, wrapperDiv);
});
}
if (wrapperDiv) {
wrapperDiv.removeEventListener('mouseleave', handleMouseLeave);
wrapperDiv.addEventListener('mouseleave', function (event) {
handleMouseLeave(event, menuContainer, wrapperDiv);
});
}
});
}
function handleMouseLeave(event, menuContainer, wrapperDiv) {
// find the menu button within the current container (e.g. this row's menu button)
var button = menuContainer.querySelector('button');
// Don't hide the menu if the cursor is on the button or the menu
if (wrapperDiv.matches(':hover') || button.matches(':hover')) {
return false;
}
if (wrapperDiv) {
// Hide the menu
wrapperDiv.classList.remove("ShowMenu");
menuContainer.appendChild(wrapperDiv); // Put the menu wrapper back in its original place so that mouseenter works next time.
}
}
function handleMouseOver(event) {
var menuButton = event.target;
if (menuButton) {
var menuContainer = menuButton.closest('.MenuContainer');
var wrapperDiv = menuContainer.querySelector('.wrapper');
if (wrapperDiv) {
// Show the menu
wrapperDiv.classList.add('ShowMenu');
// Position the menu alongside the button
var menuButtonPos = menuButton.getBoundingClientRect();
var wrapperDivPos = wrapperDiv.getBoundingClientRect();
console.log("Button left = " + menuButtonPos.left);
var newMenuPosLeft = menuButtonPos.left - wrapperDivPos.width + 5;
var newMenuPosTop = menuButtonPos.top + window.scrollY;
var resultsTable = wrapperDiv.closest('table')
if (resultsTable) {
var searchResultsGridComponent = resultsTable.closest('.SearchResultsGridComponent');
if (searchResultsGridComponent) {
// Move menu's DOM position outside the table so that it is no longer constrained by the table parent's scroll boundary.
searchResultsGridComponent.appendChild(wrapperDiv);
}
}
wrapperDiv.style.top = newMenuPosTop + 'px';
wrapperDiv.style.left = newMenuPosLeft + 'px';
// // TODO: Recheck menu position now it has been positioned
// wrapperDivPos = wrapperDiv.getBoundingClientRect();
// var bottomEdge = window.scrollY + window.innerHeight;
// if (wrapperDivPos.bottom > bottomEdge) {
// var amountOffScren = (wrapperDivPos.bottom - bottomEdge);
// wrapperDiv.style.top = (wrapperDivPos.top - amountOffScren) + 'px';
// }
}
}
}
WireUpKebabMenusOnMouseOver();
.SearchResultsGridComponent {
padding: 1rem;
width: fit-content;
max-width: 100%;
}
.SearchResultsGridComponent .SearchResultsGrid {
max-width: 100%;
overflow-x: auto;
}
.SearchResultsGridComponent .SearchResultsGrid table {
border-collapse: separate;
border-spacing: 0;
border: 1px solid #dededf;
background-color: #fff;
}
.SearchResultsGridComponent .SearchResultsGrid table thead tr th {
padding: 1rem;
text-align: left;
background-color: #dededf;
}
.SearchResultsGridComponent .SearchResultsGrid table tbody tr {
border-top: 1px solid #dededf;
}
.SearchResultsGridComponent .SearchResultsGrid table tbody tr td {
padding: 1rem;
border: 1px solid #eee;
}
.SearchResultsGridComponent .SearchResultsGrid table tbody tr td.KebabMenuContainer {
background-color: #eee;
border: 1px solid #ddd;
position: sticky;
right: -0.2rem;
}
.SearchResultsGridComponent .SearchResultsGrid table tbody tr td.KebabMenuContainer button {
background-image: url("/Images/Icons/KebabMenu.svg");
background-position: center center;
}
.MenuContainer {
line-height: 0;
border: 1px dotted #aaa;
}
.MenuContainer button {
object-fit: cover;
border: none;
background-color: #fff;
height: 3rem;
width: 3rem;
background-repeat: no-repeat;
border: 1px solid green;
}
.wrapper {
position: absolute;
/* Break out of normal page flow */
z-index: 5;
display: none;
border: 1px solid red;
background-color: #eee;
}
.wrapper > ul {
margin: 0;
padding: 0;
list-style-type: none;
}
.wrapper > ul li {
margin: 0;
padding: 0;
}
.wrapper > ul li:not(:first-of-type) {
border-top: 0.1rem solid #e8e8e8;
}
.wrapper > ul li a {
display: block;
padding: 1rem;
white-space: nowrap;
text-decoration: none;
}
.wrapper > ul li a:hover {
background-color: #e8e8e8;
}
.wrapper > ul li label {
padding: 1rem;
white-space: nowrap;
}
.wrapper > ul li label:hover {
background-color: #e8e8e8;
}
.wrapper.ShowMenu {
display: block;
}
<div class="SearchResultsGridComponent">
<div class="SearchResultsGrid">
<table role="table">
<thead role="rowgroup">
<tr>
<th role="columnheader">ID</th>
<th role="columnheader">Name</th>
<th role="columnheader">Surname</th>
<th role="columnheader">Long value</th>
<th role="columnheader"></th>
</tr>
</thead>
<tbody role="rowgroup">
<tr role="row">
<td role="cell">
1
</td>
<td role="cell">
Bob
</td>
<td role="cell">
Down
</td>
<td role="cell">
</td>
<td role="cell" class="MenuContainer KebabMenuContainer"><button type="button">Menu</button>
<div class="wrapper">
<ul>
<li><a href="#">Menu Action 1</a></li>
<li><a href="#">Menu Action 2</a></li>
<li><a href="#">Menu Action 3</a></li>
<li><a href="#">Menu Action 4</a></li>
<li><a href="#">Menu Action 5</a></li>
<li><a href="#">Menu Action 6</a></li>
<li><a href="#">Menu Action 7</a></li>
</ul>
</div>
</td>
</tr>
<tr role="row">
<td role="cell">
2
</td>
<td role="cell">
Sandy
</td>
<td role="cell">
Beach
</td>
<td role="cell">
Short text
</td>
<td role="cell" class="MenuContainer KebabMenuContainer"><button type="button">Menu</button>
<div class="wrapper">
<ul>
<li><a href="#">Menu Action 1</a></li>
<li><a href="#">Menu Action 2</a></li>
<li><a href="#">Menu Action 3</a></li>
<li><a href="#">Menu Action 4</a></li>
<li><a href="#">Menu Action 5</a></li>
<li><a href="#">Menu Action 6</a></li>
<li><a href="#">Menu Action 7</a></li>
</ul>
</div>
</td>
</tr>
<tr role="row">
<td role="cell">
3
</td>
<td role="cell">
I.P.
</td>
<td role="cell">
Freely
</td>
<td role="cell">
<strong>Removing this long text fixes the problem</strong><br/>Lorem ipsum dolor sit amet, nam melius interpretaris an, sed no eripuit bonorum petentium. Lorem ipsum dolor sit amet, nam melius interpretaris an, sed no eripuit bonorum petentium.
</td>
<td role="cell" class="MenuContainer KebabMenuContainer"><button type="button">Menu</button>
<div class="wrapper">
<ul>
<li><a href="#">Menu Action 1</a></li>
<li><a href="#">Menu Action 2</a></li>
<li><a href="#">Menu Action 3</a></li>
<li><a href="#">Menu Action 4</a></li>
<li><a href="#">Menu Action 5</a></li>
<li><a href="#">Menu Action 6</a></li>
<li><a href="#">Menu Action 7</a></li>
</ul>
</div>
</td>
</tr>
<tr role="row">
<td role="cell">
4
</td>
<td role="cell">
Ivana
</td>
<td role="cell">
Beer
</td>
<td role="cell">
<img src="https://placehold.co/600x50/png" alt="Placeholder image to force table container scrolling" /> Wide image to force table scrolling
</td>
<td role="cell" class="MenuContainer KebabMenuContainer"><button type="button">Menu</button>
<div class="wrapper">
<ul>
<li><a href="#">Menu Action 1</a></li>
<li><a href="#">Menu Action 2</a></li>
<li><a href="#">Menu Action 3</a></li>
<li><a href="#">Menu Action 4</a></li>
<li><a href="#">Menu Action 5</a></li>
<li><a href="#">Menu Action 6</a></li>
<li><a href="#">Menu Action 7</a></li>
</ul>
</div>
</td>
</tr>
<tr role="row">
<td role="cell">
5
</td>
<td role="cell">
Verner
</td>
<td role="cell">
Werner
</td>
<td role="cell">
</td>
<td role="cell" class="MenuContainer KebabMenuContainer"><button type="button">Menu</button>
<div class="wrapper">
<ul>
<li><a href="#">Menu Action 1</a></li>
<li><a href="#">Menu Action 2</a></li>
<li><a href="#">Menu Action 3</a></li>
<li><a href="#">Menu Action 4</a></li>
<li><a href="#">Menu Action 5</a></li>
<li><a href="#">Menu Action 6</a></li>
<li><a href="#">Menu Action 7</a></li>
</ul>
</div>
</td>
</tr>
<tr role="row">
<td role="cell">
6
</td>
<td role="cell">
Long
</td>
<td role="cell">
Thyme
</td>
<td role="cell">
</td>
<td role="cell" class="MenuContainer KebabMenuContainer"><button type="button">Menu</button>
<div class="wrapper">
<ul>
<li><a href="#">Menu Action 1</a></li>
<li><a href="#">Menu Action 2</a></li>
<li><a href="#">Menu Action 3</a></li>
<li><a href="#">Menu Action 4</a></li>
<li><a href="#">Menu Action 5</a></li>
<li><a href="#">Menu Action 6</a></li>
<li><a href="#">Menu Action 7</a></li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
It has emerged that Chrome calculates in advance where the scroll bar would be if it was required, and neglects the fact that the hover element is no longer in its original place. That is why the top few menus appear correctly but the latter ones don't - because the latter ones would need the scroll bar if positioned in the normal document flow.
The different behaviour in Firefox is due to Firefox using the operating system scroll bar ('overlay' scroll bar in Windows), whereas Chrome uses the 'classic scrollbar' approach ([detailed by Google here][4]). Overlay scroll bars render over content and do not affect positioning.
The workaround (for now) is to force overflow-y: scroll
to force the vertical scrollbar to always appear.