diff --git a/_scripts/popover.js b/_scripts/popover.js index 9eef9a466d..e756c74dfa 100644 --- a/_scripts/popover.js +++ b/_scripts/popover.js @@ -12,32 +12,60 @@ jQuery.then(($) => { $document.on('click', '.popover > a', function (event) { event.preventDefault() - const $body = $('body') - const $link = $(event.target) - const $popover = $link.parent() + // Stop propagation so this click doesn't immediately trigger + // the document-level close handler registered below + event.stopPropagation() + + const $popover = $(event.target).closest('.popover') const $content = $popover.find('.popover-content') - $body.css({ overflow: 'hidden' }) + if ($popover.hasClass('active')) { + $popover.removeClass('active') + return + } $popover.addClass('active') + // Prevent scroll events inside the popover content from + // bubbling up to the document close handler $content.on('scroll touchmove mousewheel wheel', function (e) { e.stopPropagation() }) - const popoverPos = ($popover.outerWidth() / 2) - ($content.outerWidth() / 2) - $content.css({ left: popoverPos }) + // Position the popover centered on its trigger, then clamp it + // within the viewport so it doesn't cause horizontal overflow + const popoverWidth = $popover.outerWidth() + const contentWidth = $content.outerWidth() + let popoverPos = (popoverWidth / 2) - (contentWidth / 2) - $document.one('click scroll touchmove mousewheel wheel', function (event) { - if (!$(event.target).is('.popover-content *')) { - event.stopImmediatePropagation() - event.preventDefault() - } + const popoverRect = $popover[0].getBoundingClientRect() + const contentRight = popoverRect.left + popoverPos + contentWidth + const viewportWidth = document.documentElement.clientWidth + + if (contentRight > viewportWidth) { + popoverPos -= (contentRight - viewportWidth) + } - $body.css({ overflow: 'visible' }) + const contentLeft = popoverRect.left + popoverPos + if (contentLeft < 0) { + popoverPos -= contentLeft + } + + // Move the arrow to stay aligned with the trigger button, + // compensating for how far the content was shifted + const arrowLeft = (popoverWidth / 2) - popoverPos + $content[0].style.setProperty('--popover-left', popoverPos + 'px') + $content[0].style.setProperty('--arrow-left', arrowLeft + 'px') + + // Close when the user interacts outside the popover content. + // Uses namespaced events so we can cleanly unbind them all at once + $document.on('click.popover scroll.popover touchmove.popover', function (e) { + if ($(e.target).closest('.popover-content').length) { + return + } $popover.removeClass('active') - $body.click() + $document.off('.popover') }) }) }) diff --git a/_styles/main.css b/_styles/main.css index 9568700458..6f12259c5f 100644 --- a/_styles/main.css +++ b/_styles/main.css @@ -19,6 +19,7 @@ body { font-weight: 400; margin: 0; min-height: 100vh; + overflow-x: hidden; } *, @@ -1238,7 +1239,7 @@ So that for browsers that dont we can have a sensible inline style that can be a bottom: 30px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); color: #333; - left: -70px; + left: var(--popover-left, -70px); opacity: 0; position: absolute; text-align: left; @@ -1254,7 +1255,7 @@ So that for browsers that dont we can have a sensible inline style that can be a content: ""; display: block; height: 0; - left: 50%; + left: var(--arrow-left, 50%); margin-left: -5px; position: absolute; width: 0;