From 5efd72040b10b1e172f5cb1f66802c86ceef7214 Mon Sep 17 00:00:00 2001 From: Suzanne Aitchison Date: Wed, 11 Aug 2021 18:40:11 +0100 Subject: [PATCH] change preview card dropdown position depending on viewport (#14473) --- .../stylesheets/components/dropdowns.scss | 6 +++ app/javascript/packs/articlePage.jsx | 21 ++++++++- app/javascript/packs/commentDropdowns.js | 11 ++++- app/javascript/utilities/dropdownUtils.js | 47 +++++++++++++++++++ .../shared/_profile_preview_card.html.erb | 2 +- 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/components/dropdowns.scss b/app/assets/stylesheets/components/dropdowns.scss index fb4c6435a..35502ead8 100644 --- a/app/assets/stylesheets/components/dropdowns.scss +++ b/app/assets/stylesheets/components/dropdowns.scss @@ -29,4 +29,10 @@ top: 0; } } + + &.reverse { + // Flips the dropdown to drop-upwards when set + bottom: 100%; + top: unset; + } } diff --git a/app/javascript/packs/articlePage.jsx b/app/javascript/packs/articlePage.jsx index e7b41d1be..f2be65f6c 100644 --- a/app/javascript/packs/articlePage.jsx +++ b/app/javascript/packs/articlePage.jsx @@ -3,7 +3,11 @@ import ahoy from 'ahoy.js'; import { Snackbar, addSnackbarItem } from '../Snackbar'; import { addFullScreenModeControl } from '../utilities/codeFullscreenModeSwitcher'; import { embedGists } from '../utilities/gist'; -import { initializeDropdown } from '@utilities/dropdownUtils'; +import { + initializeDropdown, + getDropdownRepositionListener, +} from '../utilities/dropdownUtils'; +import { getInstantClick } from '../topNavigation/utilities'; /* global Runtime */ @@ -188,3 +192,18 @@ if (profilePreviewTrigger?.dataset.initialized !== 'true') { const targetNode = document.querySelector('#comments'); targetNode && embedGists(targetNode); + +// Preview card dropdowns reposition on scroll +const dropdownRepositionListener = getDropdownRepositionListener(); + +document.addEventListener('scroll', dropdownRepositionListener); + +getInstantClick().then((ic) => { + ic.on('change', () => { + document.removeEventListener('scroll', dropdownRepositionListener); + }); +}); + +window.addEventListener('beforeunload', () => { + document.removeEventListener('scroll', dropdownRepositionListener); +}); diff --git a/app/javascript/packs/commentDropdowns.js b/app/javascript/packs/commentDropdowns.js index 4e3c6e0e2..e05338a8c 100644 --- a/app/javascript/packs/commentDropdowns.js +++ b/app/javascript/packs/commentDropdowns.js @@ -1,5 +1,8 @@ import { addSnackbarItem } from '../Snackbar'; -import { initializeDropdown } from '@utilities/dropdownUtils'; +import { + initializeDropdown, + getDropdownRepositionListener, +} from '@utilities/dropdownUtils'; /* global Runtime */ @@ -122,12 +125,18 @@ observer.observe(document.getElementById('comment-trees-container'), { subtree: true, }); +// Preview card dropdowns reposition on scroll +const dropdownRepositionListener = getDropdownRepositionListener(); +document.addEventListener('scroll', dropdownRepositionListener); + InstantClick.on('change', () => { observer.disconnect(); + document.removeEventListener('scroll', dropdownRepositionListener); }); window.addEventListener('beforeunload', () => { observer.disconnect(); + document.removeEventListener('scroll', dropdownRepositionListener); }); initializeArticlePageDropdowns(); diff --git a/app/javascript/utilities/dropdownUtils.js b/app/javascript/utilities/dropdownUtils.js index 97ca8a372..6340be5df 100644 --- a/app/javascript/utilities/dropdownUtils.js +++ b/app/javascript/utilities/dropdownUtils.js @@ -1,3 +1,50 @@ +import { isInViewport } from '@utilities/viewport'; +import { debounceAction } from '@utilities/debounceAction'; + +/** + * Helper function designed to be used on scroll to detect when dropdowns should switch from dropping downwards/upwards. + * The action is debounced since scroll events are usually fired several at a time. + * + * @returns {Function} a debounced function that handles the repositioning of dropdowns + * @example + * + * document.addEventListener('scroll', getDropdownRepositionListener()); + */ +export const getDropdownRepositionListener = () => + debounceAction(handleDropdownRepositions); + +/** + * Checks for all dropdowns on the page which have the attribute 'data-repositioning-dropdown', signalling + * they should dynamically change between dropping downwards or upwards, depending on viewport position. + * + * Any dropdowns not fully in view when dropping down will be switched to dropping upwards. + */ +const handleDropdownRepositions = () => { + // Select all of the dropdowns which should reposition + const allRepositioningDropdowns = document.querySelectorAll( + '[data-repositioning-dropdown]', + ); + + for (const element of allRepositioningDropdowns) { + // Default to dropping downwards + element.classList.remove('reverse'); + + // We can't determine position on an element with display:none, so we "show" the dropdown with 0 opacity very temporarily + element.style.opacity = 0; + element.style.display = 'block'; + const isWithinViewport = isInViewport({ element }); + + // Revert the temporary changes to determine position + element.style.removeProperty('display'); + element.style.removeProperty('opacity'); + + if (!isWithinViewport) { + // If the element isn't fully visible when dropping down, reverse the direction + element.classList.add('reverse'); + } + } +}; + /** * Helper query string to identify interactive/focusable HTML elements */ diff --git a/app/views/shared/_profile_preview_card.html.erb b/app/views/shared/_profile_preview_card.html.erb index 5fd8e6c6c..2c1bf44a6 100644 --- a/app/views/shared/_profile_preview_card.html.erb +++ b/app/views/shared/_profile_preview_card.html.erb @@ -1,4 +1,4 @@ -
+
<%= render "shared/profile_card_content", context: "preview-card", actor: actor %>