From 784625559e17cd2debee94d5931e39ab69a8c25c Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Mon, 17 Aug 2020 18:54:40 +0200 Subject: [PATCH] Overflow handling for reaction popovers on smartphones Closes #3518 --- .../WoltLabSuite/Core/Ui/Reaction/Handler.js | 49 +++++++++++++++++-- .../install/files/style/ui/reactions.scss | 45 +++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js index 7e0a60363c..5aad47ce22 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js @@ -23,6 +23,8 @@ define( { "use strict"; + var UiScreen = require('Ui/Screen'); + /** * @constructor */ @@ -175,6 +177,24 @@ define( if (~~elData(reactionTypeButton, 'is-assignable') === 0) { elShow(reactionTypeButton); } + + this._scrollReactionIntoView(reactionTypeButton); + } + }, + + _scrollReactionIntoView: function (reactionTypeButton) { + var scrollableContainer = elBySel('.reactionPopoverContent', this._getPopover()); + + // Do not scroll if the button is located in the upper 75%. + if (reactionTypeButton.offsetTop < scrollableContainer.clientHeight * 0.75) { + scrollableContainer.scrollTop = 0; + } + else { + // `Element.scrollTop` permits arbitrary values and will always clamp them to + // the maximum possible offset value. We can abuse this behavior by calculating + // the values to place the selected reaction in the center of the popover, + // regardless of the offset being out of range. + scrollableContainer.scrollTop = reactionTypeButton.offsetTop + reactionTypeButton.clientHeight / 2 - scrollableContainer.clientHeight / 2; } }, @@ -221,20 +241,30 @@ define( } this._popoverCurrentObjectId = objectId; - this._markReactionAsActive(); UiAlignment.set(this._getPopover(), element, { pointer: true, - horizontal: (this._options.isButtonGroupNavigation) ? 'left' :'center', - vertical: 'top' + horizontal: (this._options.isButtonGroupNavigation) ? 'left' : 'center', + vertical: UiScreen.is('screen-xs') ? 'bottom' : 'top' }); if (this._options.isButtonGroupNavigation) { element.closest('nav').style.setProperty('opacity', '1', ''); } - this._getPopover().classList.remove('forceHide'); - this._getPopover().classList.add('active'); + var popover = this._getPopover(); + + // The popover could be rendered below the input field on mobile, in which case + // the "first" button is displayed at the bottom and thus farthest away. Reversing + // the display order will restore the logic by placing the "first" button as close + // to the react button as possible. + var inverseOrder = popover.style.getPropertyValue('bottom') === 'auto'; + popover.classList[inverseOrder ? 'add' : 'remove']('inverseOrder'); + + this._markReactionAsActive(); + + popover.classList.remove('forceHide'); + popover.classList.add('active'); }, /** @@ -251,6 +281,7 @@ define( _popoverContent.className = 'reactionPopoverContent'; var popoverContentHTML = elCreate('ul'); + popoverContentHTML.className = 'reactionTypeButtonList'; var sortedReactionTypes = this._getSortedReactionTypes(); @@ -286,6 +317,14 @@ define( } _popoverContent.appendChild(popoverContentHTML); + _popoverContent.addEventListener('scroll', function () { + var hasTopOverflow = _popoverContent.scrollTop > 0; + _popoverContent.classList[hasTopOverflow ? 'add' : 'remove']('overflowTop'); + + var hasBottomOverflow = _popoverContent.scrollTop + _popoverContent.clientHeight < _popoverContent.scrollHeight; + _popoverContent.classList[hasBottomOverflow ? 'add' : 'remove']('overflowBottom'); + }, {passive: true}); + this._popover.appendChild(_popoverContent); var pointer = elCreate('span'); diff --git a/wcfsetup/install/files/style/ui/reactions.scss b/wcfsetup/install/files/style/ui/reactions.scss index b22db4f6ef..18e4c4ed12 100644 --- a/wcfsetup/install/files/style/ui/reactions.scss +++ b/wcfsetup/install/files/style/ui/reactions.scss @@ -4,6 +4,7 @@ background-color: $wcfContentBackground; border-radius: 2px; box-shadow: 0 2px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + overflow: hidden; position: absolute; top: 0; vertical-align: middle; @@ -23,6 +24,12 @@ > .elementPointer { display: none; } + + @include screen-xs { + &.inverseOrder .reactionTypeButtonList { + flex-direction: column-reverse; + } + } } .reactionType { @@ -86,6 +93,39 @@ } } + @include screen-xs { + max-height: 200px; + overflow: auto; + + &::after, + &::before { + content: ""; + height: 40px; + left: 0; + opacity: 0; + pointer-events: none; + position: absolute; + right: 0; + transition: opacity .12s linear; + } + + &::after { + background-image: linear-gradient(to bottom, transparent, $wcfContentBackground); + bottom: 0; + } + &.overflowBottom::after { + opacity: 1; + } + + &::before { + background-image: linear-gradient(to top, transparent, $wcfContentBackground); + top: 0; + } + &.overflowTop::before { + opacity: 1; + } + } + @include screen-md-down { padding: 5px 0; @@ -140,6 +180,11 @@ } } +.reactionTypeButtonList { + display: flex; + flex-direction: column; +} + @include screen-lg { html.touch .reactionPopoverContent .reactionTypeButton { display: block; -- 2.20.1