From b6f5b236b38d5ef00a9c1d12b39c584ba6f3b544 Mon Sep 17 00:00:00 2001 From: Kris <kris.aubuchon@discourse.org> Date: Mon, 24 Oct 2022 19:22:02 -0400 Subject: [PATCH] UX: drag new user menus, scroll primary user nav (#18690) --- .../app/components/horizontal-overflow-nav.js | 72 +++++++++++----- .../discourse/app/components/user-nav.hbs | 4 +- .../components/horizontal-overflow-nav.hbs | 8 +- .../stylesheets/common/base/new-user.scss | 84 +++---------------- .../stylesheets/common/components/_index.scss | 1 + .../components/horizontal-overflow-nav.scss | 78 +++++++++++++++++ 6 files changed, 150 insertions(+), 97 deletions(-) create mode 100644 app/assets/stylesheets/common/components/horizontal-overflow-nav.scss diff --git a/app/assets/javascripts/discourse/app/components/horizontal-overflow-nav.js b/app/assets/javascripts/discourse/app/components/horizontal-overflow-nav.js index 79d0a065190..24d2f4292f9 100644 --- a/app/assets/javascripts/discourse/app/components/horizontal-overflow-nav.js +++ b/app/assets/javascripts/discourse/app/components/horizontal-overflow-nav.js @@ -12,10 +12,8 @@ export default class HorizontalOverflowNav extends Component { scrollInterval; @bind - scrollToActive() { - const activeElement = document.querySelector( - ".user-navigation-secondary a.active" - ); + scrollToActive(element) { + const activeElement = element.querySelector("a.active"); activeElement?.scrollIntoView({ block: "nearest", @@ -24,14 +22,14 @@ export default class HorizontalOverflowNav extends Component { } @bind - checkScroll(element) { + checkScroll(event) { if (this.site.mobileView) { return; } - this.watchScroll(element); + this.watchScroll(event); return (this.hasScroll = - element.target.scrollWidth > element.target.offsetWidth); + event.target.scrollWidth > event.target.offsetWidth); } @bind @@ -40,14 +38,14 @@ export default class HorizontalOverflowNav extends Component { } @bind - watchScroll(element) { + watchScroll(event) { if (this.site.mobileView) { return; } if ( - element.target.offsetWidth + element.target.scrollLeft === - element.target.scrollWidth + event.target.offsetWidth + event.target.scrollLeft === + event.target.scrollWidth ) { this.hideRightScroll = true; clearInterval(this.scrollInterval); @@ -55,7 +53,7 @@ export default class HorizontalOverflowNav extends Component { this.hideRightScroll = false; } - if (element.target.scrollLeft === 0) { + if (event.target.scrollLeft === 0) { this.hideLeftScroll = true; clearInterval(this.scrollInterval); } else { @@ -63,21 +61,57 @@ export default class HorizontalOverflowNav extends Component { } } - @action - horizScroll(element) { - let scrollSpeed = 100; - let siblingTarget = element.target.previousElementSibling; + @bind + scrollDrag(event) { + if (this.site.mobileView) { + return; + } - if (element.target.dataset.direction === "left") { + event.preventDefault(); + + const navPills = event.target.closest(".nav-pills"); + + const position = { + left: navPills.scrollLeft, // current scroll + x: event.clientX, // mouse position + }; + + const mouseDragScroll = function (e) { + let mouseChange = e.clientX - position.x; + navPills.scrollLeft = position.left - mouseChange; + + navPills.querySelectorAll("a").forEach((a) => { + a.style.cursor = "grabbing"; + }); + }; + + const removeDragScroll = function () { + document.removeEventListener("mousemove", mouseDragScroll); + + navPills.querySelectorAll("a").forEach((a) => { + a.style.cursor = "pointer"; + }); + }; + + document.addEventListener("mousemove", mouseDragScroll); + document.addEventListener("mouseup", removeDragScroll); + } + + @action + horizScroll(event) { + let scrollSpeed = 175; + let siblingTarget = event.target.previousElementSibling; + + if (event.target.dataset.direction === "left") { scrollSpeed = scrollSpeed * -1; - siblingTarget = element.target.nextElementSibling; + siblingTarget = event.target.nextElementSibling; } this.scrollInterval = setInterval(function () { siblingTarget.scrollLeft += scrollSpeed; }, 50); - element.target.addEventListener("mouseup", this.stopScroll); - element.target.addEventListener("mouseleave", this.stopScroll); + event.target.addEventListener("mouseup", this.stopScroll); + event.target.addEventListener("mouseleave", this.stopScroll); } } diff --git a/app/assets/javascripts/discourse/app/components/user-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav.hbs index 6a832c2d89f..4caa7d2e7a6 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav.hbs @@ -1,5 +1,5 @@ <section class="user-navigation user-navigation-primary"> - <ul class="main-nav nav nav-pills user-nav"> + <HorizontalOverflowNav @className="main-nav nav user-nav"> {{#unless @user.profile_hidden}} <li class="summary"> <LinkTo @route="user.summary"> @@ -74,5 +74,5 @@ </a> </li> {{/if}} - </ul> + </HorizontalOverflowNav> </section> diff --git a/app/assets/javascripts/discourse/app/templates/components/horizontal-overflow-nav.hbs b/app/assets/javascripts/discourse/app/templates/components/horizontal-overflow-nav.hbs index d726eb8eefc..e3963b966e4 100644 --- a/app/assets/javascripts/discourse/app/templates/components/horizontal-overflow-nav.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/horizontal-overflow-nav.hbs @@ -1,4 +1,5 @@ {{!-- template-lint-disable no-down-event-binding --}} +{{!-- template-lint-disable no-invalid-interactive --}} <div class="horizontal-overflow-nav {{if this.hasScroll "has-scroll"}}"> {{#if this.hasScroll}} @@ -6,7 +7,7 @@ role="button" {{on "mousedown" this.horizScroll}} data-direction="left" - class="nav-overflow__scroll-left btn-flat {{if this.hideLeftScroll "transparent"}}" + class="horizontal-overflow-nav__scroll-left {{if this.hideLeftScroll "disabled"}}" > {{d-icon "chevron-left"}} </a> @@ -16,7 +17,8 @@ {{on-resize this.checkScroll}} {{on "scroll" this.watchScroll}} {{did-insert this.scrollToActive}} - class="nav-pills action-list" + {{on "mousedown" this.scrollDrag}} + class="nav-pills action-list {{@className}}" > {{yield}} </ul> @@ -25,7 +27,7 @@ <a role="button" {{on "mousedown" this.horizScroll}} - class="nav-overflow__scroll-right btn-flat {{if this.hideRightScroll "transparent"}}" + class="horizontal-overflow-nav__scroll-right {{if this.hideRightScroll "disabled"}}" > {{d-icon "chevron-right"}} </a> diff --git a/app/assets/stylesheets/common/base/new-user.scss b/app/assets/stylesheets/common/base/new-user.scss index c666ce84612..85772e3fe3e 100644 --- a/app/assets/stylesheets/common/base/new-user.scss +++ b/app/assets/stylesheets/common/base/new-user.scss @@ -2,9 +2,8 @@ margin-top: -15px; // temp, can remove margin from sibling element after nav finalized .user-navigation { --user-navigation__border-width: 4px; - &.user-navigation-primary { - border-bottom: 1px solid var(--primary-low); - } + border-bottom: 1px solid var(--primary-low); + .nav-pills { width: 100%; margin: 0; @@ -37,7 +36,6 @@ li { flex: 1 1 auto; margin: 0; - overflow: hidden; display: flex; a { @@ -80,6 +78,15 @@ } } + .user-navigation-primary { + [class*="horizontal-overflow-nav__scroll"] { + font-size: var(--font-up-1); + .d-icon { + margin-top: 0.15em; // minor alignment + } + } + } + .user-navigation-secondary { --user-navigation__border-width: 2px; position: relative; @@ -89,12 +96,6 @@ gap: 0 0.5em; border-bottom: 1px solid var(--primary-low); - .horizontal-overflow-nav { - position: relative; - min-width: 0; - width: 100%; - } - .select-kit .select-kit-header { height: 100%; padding: 0.5em 1em; @@ -116,73 +117,10 @@ } } - .nav-overflow__scroll-right, - .nav-overflow__scroll-left { - --fade-width: 20px; - opacity: 1; - position: absolute; - z-index: 2; - background-color: var(--secondary); - top: 0; - bottom: 0; - display: flex; - align-items: center; - transition: opacity 0.25s; - .d-icon { - pointer-events: none; - margin-bottom: 0.2em; - color: var(--quaternary); - } - &.transparent { - // hiding with opacity so we can transition visibility - opacity: 0; - pointer-events: none; - } - } - - .nav-overflow__scroll-right { - right: 0; - &:before { - content: ""; - margin-left: -1.5em; - height: 100%; - width: 1.5em; - background: linear-gradient( - to left, - rgba(var(--secondary-rgb), 1), - rgba(var(--secondary-rgb), 0) - ); - } - } - - .nav-overflow__scroll-left { - left: 0; - &:after { - content: ""; - margin-right: -1.5em; - height: 100%; - width: 1.5em; - background: linear-gradient( - to right, - rgba(var(--secondary-rgb), 1), - rgba(var(--secondary-rgb), 0) - ); - } - } - .nav-pills { flex: 1 1 auto; font-size: var(--font-down-1); justify-content: flex-start; - overflow: auto; - position: relative; - scroll-behavior: smooth; - - // hides scrollbars, but allows mouse scrolling - scrollbar-width: none; - &::-webkit-scrollbar { - height: 0; - } li { flex: 1 0 auto; diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss index a054cd44783..361546e0532 100644 --- a/app/assets/stylesheets/common/components/_index.scss +++ b/app/assets/stylesheets/common/components/_index.scss @@ -15,6 +15,7 @@ @import "group-member-dropdown"; @import "groups-form-membership-fields"; @import "hashtag"; +@import "horizontal-overflow-nav"; @import "iframed-html"; @import "ignored-user-list"; @import "keyboard_shortcuts"; diff --git a/app/assets/stylesheets/common/components/horizontal-overflow-nav.scss b/app/assets/stylesheets/common/components/horizontal-overflow-nav.scss new file mode 100644 index 00000000000..9f2f817c02e --- /dev/null +++ b/app/assets/stylesheets/common/components/horizontal-overflow-nav.scss @@ -0,0 +1,78 @@ +.horizontal-overflow-nav { + position: relative; + min-width: 0; + width: 100%; + .nav-pills { + overflow: auto; + min-width: 0; + position: relative; + + // avoids auto-scroll on initial load if active nav item is overflowed + scroll-behavior: auto; + + // hides scrollbars, but allows mouse scrolling + scrollbar-width: none; + &::-webkit-scrollbar { + height: 0; + } + } + &.has-scroll { + .nav-pills { + scroll-behavior: smooth; // smooth scrolling on user interaction + } + } +} + +.horizontal-overflow-nav__scroll-right, +.horizontal-overflow-nav__scroll-left { + --fade-width: 1.5em; + opacity: 1; + position: absolute; + z-index: 2; + background-color: var(--secondary); + top: 0; + bottom: 0; + display: flex; + align-items: center; + transition: opacity 0.25s; + .d-icon { + pointer-events: none; + margin-bottom: 0.2em; + color: var(--quaternary); + } + &.disabled { + // hiding with opacity so we can transition visibility + opacity: 0; + pointer-events: none; + } +} + +.horizontal-overflow-nav__scroll-right { + right: 0; + &:before { + content: ""; + margin-left: calc(var(--fade-width) * -1); + height: 100%; + width: var(--fade-width); + background: linear-gradient( + to left, + rgba(var(--secondary-rgb), 1), + rgba(var(--secondary-rgb), 0) + ); + } +} + +.horizontal-overflow-nav__scroll-left { + left: 0; + &:after { + content: ""; + margin-right: calc(var(--fade-width) * -1); + height: 100%; + width: var(--fade-width); + background: linear-gradient( + to right, + rgba(var(--secondary-rgb), 1), + rgba(var(--secondary-rgb), 0) + ); + } +}