From 208d788672e92a2f3a6d352fb1bc265ab5951be4 Mon Sep 17 00:00:00 2001 From: Hariharan Sampath Date: Wed, 14 Apr 2021 08:05:50 +0530 Subject: [PATCH] Fix feed navigation shortcuts when first article is not 100% visible (#11874) Co-authored-by: Nick Taylor Co-authored-by: Hariharan --- .../shared/components/useListNavigation.js | 22 ++++++++--- app/javascript/utilities/viewport.js | 39 +++++++++++++++---- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/app/javascript/shared/components/useListNavigation.js b/app/javascript/shared/components/useListNavigation.js index ebc280c73..97478a996 100644 --- a/app/javascript/shared/components/useListNavigation.js +++ b/app/javascript/shared/components/useListNavigation.js @@ -152,7 +152,7 @@ function navigate( const nextFocusable = nextContainer?.querySelector(focusableSelector); if (nextFocusable) { nextFocusable.focus(); - if (!isInViewport(nextFocusable, 64)) { + if (!isInViewport({ element: nextFocusable, offsetTop: 64 })) { window.scrollTo({ top: nextContainer.offsetTop - 64 }); } } @@ -211,15 +211,27 @@ function getPreviousElement( } /** - * Gets the first visible element that matches a selector + * Checks if the first completely visible element is present that matches a selector and returns if it is available + * If that isn't visible, it looks for the partially visible element that is present and returns that + * If no elements visible(like the banner case which could cover the entire viewport), we select the first element from the list * * @param {string} selector - The CSS selector * * @returns {object} The first visible element */ function getFirstVisibleElement(selector) { - const elements = document.querySelectorAll(selector); - return Array.prototype.find.call(elements, (element) => - isInViewport(element), + const elements = [...document.querySelectorAll(selector)]; + const completelyVisibleFirstElement = elements.find((element) => + isInViewport({ element }), ); + if (completelyVisibleFirstElement) { + return completelyVisibleFirstElement; + } + const partiallyVisibleFirstElement = elements.find((element) => + isInViewport({ element, allowPartialVisibility: true }), + ); + if (partiallyVisibleFirstElement) { + return partiallyVisibleFirstElement; + } + return elements[0]; } diff --git a/app/javascript/utilities/viewport.js b/app/javascript/utilities/viewport.js index 920a190ad..c3eb5489e 100644 --- a/app/javascript/utilities/viewport.js +++ b/app/javascript/utilities/viewport.js @@ -3,22 +3,45 @@ * * @example * const element = document.getElementById('element'); - * isInViewport(element); // true or false + * isInViewport({element, allowPartialVisibility = true}); // true or false * - * @param {object} element - The HTML element to check + * @param {HTMLElement} element - The HTML element to check * @param {number} [offsetTop=0] - Part of the screen to ignore counting from the top - * + * @param {boolean} [allowPartialVisibility=false] - A boolean to flip the check between partial or completely visible in the viewport * @returns {boolean} isInViewport - true if the element is visible in the viewport */ -export function isInViewport(element, offsetTop = 0) { +export function isInViewport({ + element, + offsetTop = 0, + allowPartialVisibility = false, +}) { const boundingRect = element.getBoundingClientRect(); const clientHeight = window.innerHeight || document.documentElement.clientHeight; const clientWidth = window.innerWidth || document.documentElement.clientWidth; + const topIsInViewport = + boundingRect.top <= clientHeight && boundingRect.top >= offsetTop; + const rightIsInViewport = + boundingRect.right >= 0 && boundingRect.right <= clientWidth; + const bottomIsInViewport = + boundingRect.bottom >= offsetTop && boundingRect.bottom <= clientHeight; + const leftIsInViewport = + boundingRect.left <= clientWidth && boundingRect.left >= 0; + const topIsOutOfViewport = boundingRect.top <= offsetTop; + const bottomIsOutOfViewport = boundingRect.bottom >= clientHeight; + const elementSpansEntireViewport = + topIsOutOfViewport && bottomIsOutOfViewport; + + if (allowPartialVisibility) { + return ( + (topIsInViewport || bottomIsInViewport || elementSpansEntireViewport) && + (leftIsInViewport || rightIsInViewport) + ); + } return ( - boundingRect.top >= offsetTop && - boundingRect.left >= 0 && - boundingRect.bottom <= clientHeight && - boundingRect.right <= clientWidth + topIsInViewport && + bottomIsInViewport && + leftIsInViewport && + rightIsInViewport ); }