From 5b6604f5a7e1c8ff29fe556236dbf86872908b03 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Fri, 25 Nov 2022 15:33:26 +1100 Subject: [PATCH] FEATURE: sidebar for narrow desktop screen (#19160) When desktop screen is narrow like < 1100px, sidebar should behave similarly to mobile version. --- .../discourse/app/components/site-header.js | 30 ++++++++++-- .../discourse/app/controllers/application.js | 4 +- .../app/initializers/narrow-desktop.js | 45 ++++++++++++++++++ .../discourse/app/lib/narrow-desktop.js | 16 +++++++ .../discourse/app/widgets/header.js | 9 ++-- .../discourse/app/widgets/sidebar-toggle.js | 8 +++- .../acceptance/sidebar-narrow-desktop-test.js | 47 +++++++++++++++++++ .../stylesheets/common/base/menu-panel.scss | 47 +++++++++++++++++++ app/assets/stylesheets/mobile/menu-panel.scss | 47 ------------------- 9 files changed, 194 insertions(+), 59 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/initializers/narrow-desktop.js create mode 100644 app/assets/javascripts/discourse/app/lib/narrow-desktop.js create mode 100644 app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index 5cdb3502cf5..ceba5d0d9ee 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -40,6 +40,20 @@ const SiteHeaderComponent = MountWidget.extend( this.queueRerender(); }, + @observes("site.narrowDesktopView") + narrowDesktopViewChanged() { + if ( + this.siteSettings.enable_experimental_sidebar_hamburger && + (!this.sidebarEnabled || this.site.narrowDesktopView) + ) { + this.appEvents.on( + "sidebar-hamburger-dropdown:rendered", + this, + "_animateMenu" + ); + } + }, + _animateOpening(panel) { const headerCloak = document.querySelector(".header-cloak"); panel.classList.add("animate"); @@ -219,7 +233,7 @@ const SiteHeaderComponent = MountWidget.extend( if ( this.siteSettings.enable_experimental_sidebar_hamburger && - !this.sidebarEnabled + (!this.sidebarEnabled || this.site.narrowDesktopView) ) { this.appEvents.on( "sidebar-hamburger-dropdown:rendered", @@ -353,14 +367,17 @@ const SiteHeaderComponent = MountWidget.extend( const menuPanels = document.querySelectorAll(".menu-panel"); if (menuPanels.length === 0) { - if (this.site.mobileView) { + if (this.site.mobileView || this.site.narrowDesktopView) { this._animate = true; } return; } const windowWidth = document.body.offsetWidth; - const viewMode = this.site.mobileView ? "slide-in" : "drop-down"; + const viewMode = + this.site.mobileView || this.site.narrowDesktopView + ? "slide-in" + : "drop-down"; menuPanels.forEach((panel) => { const headerCloak = document.querySelector(".header-cloak"); @@ -377,7 +394,7 @@ const SiteHeaderComponent = MountWidget.extend( panel.classList.add(viewMode); if (this._animate || this._panMenuOffset !== 0) { if ( - this.site.mobileView && + (this.site.mobileView || this.site.narrowDesktopView) && panel.parentElement.classList.contains(this._leftMenuClass()) ) { this._panMenuOrigin = "left"; @@ -394,7 +411,7 @@ const SiteHeaderComponent = MountWidget.extend( // We use a mutationObserver to check for style changes, so it's important // we don't set it if it doesn't change. Same goes for the panelBody! - if (!this.site.mobileView) { + if (!this.site.mobileView && !this.site.narrowDesktopView) { const buttonPanel = document.querySelectorAll("header ul.icons"); if (buttonPanel.length === 0) { return; @@ -469,6 +486,8 @@ export default SiteHeaderComponent.extend({ didInsertElement() { this._super(...arguments); + this.appEvents.on("site-header:force-refresh", this, "queueRerender"); + const header = document.querySelector(".d-header-wrap"); if (header) { schedule("afterRender", () => { @@ -499,6 +518,7 @@ export default SiteHeaderComponent.extend({ this._super(...arguments); this._resizeObserver?.disconnect(); + this.appEvents.off("site-header:force-refresh", this, "queueRerender"); }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/application.js b/app/assets/javascripts/discourse/app/controllers/application.js index 1f4c3c7cd59..8e5a28645de 100644 --- a/app/assets/javascripts/discourse/app/controllers/application.js +++ b/app/assets/javascripts/discourse/app/controllers/application.js @@ -20,7 +20,9 @@ export default Controller.extend({ init() { this._super(...arguments); this.showSidebar = - this.canDisplaySidebar && !this.keyValueStore.getItem(HIDE_SIDEBAR_KEY); + this.canDisplaySidebar && + !this.keyValueStore.getItem(HIDE_SIDEBAR_KEY) && + !this.site.narrowDesktopView; }, @discourseComputed diff --git a/app/assets/javascripts/discourse/app/initializers/narrow-desktop.js b/app/assets/javascripts/discourse/app/initializers/narrow-desktop.js new file mode 100644 index 00000000000..fdd5d769bdd --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/narrow-desktop.js @@ -0,0 +1,45 @@ +import NarrowDesktop from "discourse/lib/narrow-desktop"; + +export default { + name: "narrow-desktop", + + initialize(container) { + NarrowDesktop.init(); + let site; + if (!container.isDestroyed) { + site = container.lookup("service:site"); + site.set("narrowDesktopView", NarrowDesktop.narrowDesktopView); + } + + if ("ResizeObserver" in window) { + this._resizeObserver = new ResizeObserver((entries) => { + if (container.isDestroyed) { + return; + } + for (let entry of entries) { + const oldNarrowDesktopView = site.narrowDesktopView; + const newNarrowDesktopView = NarrowDesktop.isNarrowDesktopView( + entry.contentRect.width + ); + if (oldNarrowDesktopView !== newNarrowDesktopView) { + const applicationController = container.lookup( + "controller:application" + ); + site.set("narrowDesktopView", newNarrowDesktopView); + if (newNarrowDesktopView) { + applicationController.set("showSidebar", false); + } + applicationController.appEvents.trigger( + "site-header:force-refresh" + ); + } + } + }); + + const bodyElement = document.querySelector("body"); + if (bodyElement) { + this._resizeObserver.observe(bodyElement); + } + } + }, +}; diff --git a/app/assets/javascripts/discourse/app/lib/narrow-desktop.js b/app/assets/javascripts/discourse/app/lib/narrow-desktop.js new file mode 100644 index 00000000000..96c56f82c3a --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/narrow-desktop.js @@ -0,0 +1,16 @@ +let narrowDesktopForced = false; + +const NarrowDesktop = { + narrowDesktopView: false, + + init() { + this.narrowDesktopView = + narrowDesktopForced || this.isNarrowDesktopView(window.innerWidth); + }, + + isNarrowDesktopView(width) { + return width < 1100; + }, +}; + +export default NarrowDesktop; diff --git a/app/assets/javascripts/discourse/app/widgets/header.js b/app/assets/javascripts/discourse/app/widgets/header.js index 390e3fcdfec..cd32571a51e 100644 --- a/app/assets/javascripts/discourse/app/widgets/header.js +++ b/app/assets/javascripts/discourse/app/widgets/header.js @@ -476,7 +476,7 @@ export default createWidget("header", { ); } else if (state.hamburgerVisible) { if (this.siteSettings.enable_experimental_sidebar_hamburger) { - if (!attrs.sidebarEnabled) { + if (!attrs.sidebarEnabled || this.site.narrowDesktopView) { panels.push(this.attach("revamped-hamburger-menu-wrapper", {})); } } else { @@ -501,7 +501,7 @@ export default createWidget("header", { } }); - if (this.site.mobileView) { + if (this.site.mobileView || this.site.narrowDesktopView) { panels.push(this.attach("header-cloak")); } @@ -591,7 +591,8 @@ export default createWidget("header", { toggleHamburger() { if ( this.siteSettings.enable_experimental_sidebar_hamburger && - this.attrs.sidebarEnabled + this.attrs.sidebarEnabled && + !this.site.narrowDesktopView ) { this.sendWidgetAction("toggleSidebar"); } else { @@ -601,7 +602,7 @@ export default createWidget("header", { schedule("afterRender", () => { if (this.siteSettings.enable_experimental_sidebar_hamburger) { // Remove focus from hamburger toggle button - document.querySelector("#toggle-hamburger-menu").blur(); + document.querySelector("#toggle-hamburger-menu")?.blur(); } else { // auto focus on first link in dropdown document.querySelector(".hamburger-panel .menu-links a")?.focus(); diff --git a/app/assets/javascripts/discourse/app/widgets/sidebar-toggle.js b/app/assets/javascripts/discourse/app/widgets/sidebar-toggle.js index 72cc52c6e8c..45e4699a1ab 100644 --- a/app/assets/javascripts/discourse/app/widgets/sidebar-toggle.js +++ b/app/assets/javascripts/discourse/app/widgets/sidebar-toggle.js @@ -11,8 +11,12 @@ export default createWidget("sidebar-toggle", { ? "sidebar.hide_sidebar" : "sidebar.show_sidebar", icon: "bars", - action: "toggleSidebar", - className: "btn btn-flat btn-sidebar-toggle", + action: this.site.narrowDesktopView + ? "toggleHamburger" + : "toggleSidebar", + className: `btn btn-flat btn-sidebar-toggle ${ + this.site.narrowDesktopView ? "narrow-desktop" : "" + }`, ariaExpanded: attrs.showSidebar ? "true" : "false", ariaControls: "d-sidebar", }), diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js new file mode 100644 index 00000000000..aec5992430a --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js @@ -0,0 +1,47 @@ +import { test } from "qunit"; + +import { click, settled, visit, waitUntil } from "@ember/test-helpers"; + +import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers"; + +acceptance("Sidebar - Narrow Desktop", function (needs) { + needs.user(); + + needs.settings({ + enable_experimental_sidebar_hamburger: true, + enable_sidebar: true, + }); + + test("wide sidebar is changed to cloak when resize to narrow screen", async function (assert) { + await visit("/"); + await settled(); + assert.ok(exists("#d-sidebar"), "wide sidebar is displayed"); + + await click(".header-sidebar-toggle .btn"); + + assert.ok(!exists("#d-sidebar"), "widge sidebar is collapsed"); + + const bodyElement = document.querySelector("body"); + bodyElement.style.width = "1000px"; + + await waitUntil( + () => document.querySelector(".btn-sidebar-toggle.narrow-desktop"), + { + timeout: 5000, + } + ); + await click(".btn-sidebar-toggle"); + + assert.ok( + exists(".sidebar-hamburger-dropdown"), + "cloak sidebar is displayed" + ); + + await click("#main-outlet"); + assert.ok( + !exists(".sidebar-hamburger-dropdown"), + "cloak sidebar is collapsed" + ); + bodyElement.style.width = null; + }); +}); diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 4289530592a..a4ed4233774 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -660,3 +660,50 @@ div.menu-links-header { color: var(--primary-med-or-secondary-med); } } + +.hamburger-panel .menu-panel.slide-in { + left: 0; + + .panel-body { + display: block; + } + .panel-body-contents { + max-height: unset; + min-height: 100%; + } +} +.header-cloak { + height: 100%; + width: 100%; + position: fixed; + background-color: black; + --opacity: 0.5; + opacity: var(--opacity); + top: 0; + left: 0; + display: none; + touch-action: pan-y pinch-zoom; + + @media (prefers-reduced-motion: no-preference) { + &.animate { + transition: opacity 0.1s linear; + } + } +} + +.menu-panel.slide-in { + transform: translateX(var(--offset)); + @media (prefers-reduced-motion: no-preference) { + &.animate { + transition: transform 0.1s linear; + } + } + &.moving, + &.animate { + // PERF: only render first 20 items in a list to allow for smooth + // pan events + li:nth-child(n + 20) { + display: none; + } + } +} diff --git a/app/assets/stylesheets/mobile/menu-panel.scss b/app/assets/stylesheets/mobile/menu-panel.scss index b73a77b61e3..c5d8a744704 100644 --- a/app/assets/stylesheets/mobile/menu-panel.scss +++ b/app/assets/stylesheets/mobile/menu-panel.scss @@ -1,50 +1,3 @@ -.hamburger-panel .menu-panel.slide-in { - left: 0; - - .panel-body { - display: block; - } - .panel-body-contents { - max-height: unset; - min-height: 100%; - } -} -.header-cloak { - height: 100%; - width: 100%; - position: fixed; - background-color: black; - --opacity: 0.5; - opacity: var(--opacity); - top: 0; - left: 0; - display: none; - touch-action: pan-y pinch-zoom; - - @media (prefers-reduced-motion: no-preference) { - &.animate { - transition: opacity 0.1s linear; - } - } -} - -.menu-panel.slide-in { - transform: translateX(var(--offset)); - @media (prefers-reduced-motion: no-preference) { - &.animate { - transition: transform 0.1s linear; - } - } - &.moving, - &.animate { - // PERF: only render first 20 items in a list to allow for smooth - // pan events - li:nth-child(n + 20) { - display: none; - } - } -} - .user-menu .quick-access-panel.quick-access-profile li:not(.show-all) { border-bottom: 1px solid var(--primary-low);