UX: mobile experimental sidebar improvement (#17302)

First pass at the mobile experimental sidebar improvement.

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
This commit is contained in:
Kris 2022-07-04 23:45:02 -04:00 committed by GitHub
parent 408ce1312b
commit 56c0d8cf92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 235 additions and 48 deletions

View File

@ -1,3 +1,39 @@
import GlimmerComponent from "discourse/components/glimmer";
import { bind } from "discourse-common/utils/decorators";
export default class Sidebar extends GlimmerComponent {}
export default class Sidebar extends GlimmerComponent {
constructor() {
super(...arguments);
if (this.site.mobileView) {
document.addEventListener("click", this.collapseSidebar);
}
}
@bind
collapseSidebar(event) {
let shouldCollapseSidebar = false;
const isClickWithinSidebar = event.composedPath().some((element) => {
if (
element?.className !== "sidebar-section-header-caret" &&
["A", "BUTTON"].includes(element.nodeName)
) {
shouldCollapseSidebar = true;
return true;
}
return element.className && element.className === "sidebar-wrapper";
});
if (shouldCollapseSidebar || !isClickWithinSidebar) {
this.args.toggleSidebar();
}
}
willDestroy() {
if (this.site.mobileView) {
document.removeEventListener("click", this.collapseSidebar);
}
}
}

View File

@ -1,12 +1,23 @@
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default Controller.extend({
showTop: true,
showFooter: false,
router: service(),
showSidebar: true,
showSidebar: null,
hideSidebarKey: "sidebar-hidden",
init() {
this._super(...arguments);
this.showSidebar = this.site.mobileView
? false
: this.currentUser && !this.keyValueStore.getItem(this.hideSidebarKey);
},
@discourseComputed
canSignUp() {
@ -26,4 +37,31 @@ export default Controller.extend({
showFooterNav() {
return this.capabilities.isAppWebview || this.capabilities.isiOSPWA;
},
_mainOutletAnimate() {
document
.querySelector("#main-outlet")
.classList.remove("main-outlet-animate");
},
@action
toggleSidebar() {
// enables CSS transitions, but not on did-insert
document.querySelector("body").classList.add("sidebar-animate");
// reduces CSS transition jank
document.querySelector("#main-outlet").classList.add("main-outlet-animate");
discourseDebounce(this, this._mainOutletAnimate, 250);
this.toggleProperty("showSidebar");
if (!this.site.mobileView) {
if (this.showSidebar) {
this.keyValueStore.removeItem(this.hideSidebarKey);
} else {
this.keyValueStore.setItem(this.hideSidebarKey);
}
}
},
});

View File

@ -47,13 +47,6 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
});
},
toggleSidebar() {
// enables CSS transitions, but not on did-insert
document.querySelector("body").classList.add("sidebar-animate");
this.controllerFor("application").toggleProperty("showSidebar");
},
toggleMobileView() {
mobile.toggleMobileView();
},

View File

@ -2,15 +2,19 @@
<a href="#main-container" id="skip-link">{{i18n "skip_to_main_content"}}</a>
<DDocument />
<PluginOutlet @name="above-site-header" @connectorTagName="div" />
<SiteHeader @canSignUp={{canSignUp}} @showCreateAccount={{route-action "showCreateAccount"}} @showLogin={{route-action "showLogin"}} @showKeyboard={{route-action "showKeyboardShortcutsHelp"}} @toggleMobileView={{route-action "toggleMobileView"}} @toggleAnonymous={{route-action "toggleAnonymous"}} @logout={{route-action "logout"}} @toggleSidebar={{route-action "toggleSidebar"}} />
<SiteHeader @canSignUp={{canSignUp}} @showCreateAccount={{route-action "showCreateAccount"}} @showLogin={{route-action "showLogin"}} @showKeyboard={{route-action "showKeyboardShortcutsHelp"}} @toggleMobileView={{route-action "toggleMobileView"}} @toggleAnonymous={{route-action "toggleAnonymous"}} @logout={{route-action "logout"}} @toggleSidebar={{action "toggleSidebar"}} />
<SoftwareUpdatePrompt />
<PluginOutlet @name="below-site-header" @connectorTagName="div" @args={{hash currentPath=router._router.currentPath}} />
<div id="main-outlet-wrapper" class="wrap" role="main">
{{#if (and currentUser.experimental_sidebar_enabled showSidebar)}}
<Sidebar />
{{/if}}
<div class="sidebar-wrapper">
{{!-- empty div allows for animation --}}
{{#if (and currentUser.experimental_sidebar_enabled showSidebar)}}
<Sidebar @toggleSidebar={{action "toggleSidebar"}}/>
{{/if}}
</div>
<div id="main-outlet">
<PluginOutlet @name="above-main-container" @connectorTagName="div" />

View File

@ -1,16 +1,14 @@
<DSection @pageClass="has-sidebar" @class="sidebar-wrapper">
<div class="sidebar-container">
<div class="sidebar-scroll-wrap">
<Sidebar::TopicsSection />
<Sidebar::CategoriesSection />
<DSection @pageClass="has-sidebar" @class="sidebar-container">
<div class="sidebar-scroll-wrap">
<Sidebar::TopicsSection />
<Sidebar::CategoriesSection />
{{#if this.siteSettings.tagging_enabled}}
<Sidebar::TagsSection />
{{/if}}
{{#if this.siteSettings.tagging_enabled}}
<Sidebar::TagsSection />
{{/if}}
{{#if this.siteSettings.enable_personal_messages}}
<Sidebar::MessagesSection />
{{/if}}
</div>
{{#if this.siteSettings.enable_personal_messages}}
<Sidebar::MessagesSection />
{{/if}}
</div>
</DSection>

View File

@ -9,7 +9,7 @@ export default createWidget("sidebar-toggle", {
title: "",
icon: "bars",
action: "toggleSidebar",
className: "btn btn-flat",
className: "btn btn-flat btn-sidebar-toggle",
}),
];
},

View File

@ -0,0 +1,64 @@
import { test } from "qunit";
import { click, visit } from "@ember/test-helpers";
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
acceptance("Sidebar - Mobile - User with sidebar enabled", function (needs) {
needs.user({ experimental_sidebar_enabled: true });
needs.mobileView();
test("hidden by default", async function (assert) {
await visit("/");
assert.ok(!exists(".sidebar-container"), "sidebar is not displayed");
});
test("clicking outside sidebar collapses it", async function (assert) {
await visit("/");
await click(".btn-sidebar-toggle");
assert.ok(exists(".sidebar-container"), "sidebar is displayed");
await click("#main-outlet");
assert.ok(!exists(".sidebar-container"), "sidebar is collapsed");
});
test("clicking on a link or button in sidebar collapses it", async function (assert) {
await visit("/");
await click(".btn-sidebar-toggle");
await click(".sidebar-section-link-tracked");
assert.ok(
!exists(".sidebar-container"),
"sidebar is collapsed when a button in sidebar is clicked"
);
await click(".btn-sidebar-toggle");
await click(".sidebar-section-header-link");
assert.ok(
!exists(".sidebar-container"),
"sidebar is collapsed when a link in sidebar is clicked"
);
});
test("collpasing sidebar sections does not collapse sidebar", async function (assert) {
await visit("/");
await click(".btn-sidebar-toggle");
await click(".sidebar-section-header-caret");
assert.ok(
!exists(".sidebar-section-topics .sidebar-section-content"),
"topics section is collapsed"
);
assert.ok(
exists(".sidebar-container"),
"sidebar is not collapsed when clicking on caret to collapse a section in sidebar"
);
});
});

View File

@ -14,7 +14,7 @@ acceptance("Sidebar - Anon User", function () {
"does not add sidebar utility class to body"
);
assert.ok(!exists(".sidebar-wrapper"));
assert.ok(!exists(".sidebar-container"));
});
});
@ -30,7 +30,7 @@ acceptance("Sidebar - User with sidebar disabled", function (needs) {
"does not add sidebar utility class to body"
);
assert.ok(!exists(".sidebar-wrapper"));
assert.ok(!exists(".sidebar-container"));
});
});
@ -46,7 +46,7 @@ acceptance("Sidebar - User with sidebar enabled", function (needs) {
"adds sidebar utility class to body"
);
assert.ok(exists(".sidebar-wrapper"), "displays the sidebar by default");
assert.ok(exists(".sidebar-container"), "displays the sidebar by default");
await click(".header-sidebar-toggle .btn");
@ -56,10 +56,10 @@ acceptance("Sidebar - User with sidebar enabled", function (needs) {
"removes sidebar utility class to body"
);
assert.ok(!exists(".sidebar-wrapper"), "hides the sidebar");
assert.ok(!exists(".sidebar-container"), "hides the sidebar");
await click(".header-sidebar-toggle .btn");
assert.ok(exists(".sidebar-wrapper"), "displays the sidebar");
assert.ok(exists(".sidebar-container"), "displays the sidebar");
});
});

View File

@ -671,6 +671,22 @@ table {
// Special elements
#main-outlet-wrapper {
box-sizing: border-box;
width: 100%;
display: grid;
// we can CSS transition the sidebar grid width change
// as long as we keep the column count consistent
// grid transitions are only supported in Firefox at the moment, but coming summer 2022 to Chrome
grid-template-areas: "sidebar content";
grid-template-columns: min-content minmax(0, 1fr);
gap: 0;
#main-outlet {
grid-area: content;
}
}
#main-outlet {
padding-top: 2.5em;
}

View File

@ -131,7 +131,7 @@
.sidebar-section-link {
display: flex;
align-items: center;
padding: 0.25em 0.5em;
padding: 0.35em 0.5em;
color: var(--primary-high);
font-size: var(--font-down-1);

View File

@ -187,22 +187,6 @@ input {
}
}
#main-outlet-wrapper {
box-sizing: border-box;
width: 100%;
display: grid;
// we can CSS transition the sidebar grid width change
// as long as we keep the column count consistent
// grid transitions are only supported in Firefox at the moment, but coming summer 2022 to Chrome
grid-template-areas: "sidebar content";
grid-template-columns: 0 minmax(0, 1fr);
gap: 0;
#main-outlet {
grid-area: content;
}
}
body.has-sidebar-page {
.wrap {
// increase page max-width to accommodate sidebar width

View File

@ -134,3 +134,57 @@ blockquote {
#simple-container {
width: 90%;
}
#main-outlet-wrapper {
width: 100%;
#main-outlet {
&.main-outlet-animate {
width: 95vw; // prevents content width shift during animation
}
}
}
.sidebar-wrapper {
width: 0;
transition: width 0.25s ease-in-out;
}
body.has-sidebar-page {
position: fixed;
height: calc(100vh - var(--header-offset));
#main {
overflow: hidden;
}
.sidebar-wrapper {
width: var(--d-sidebar-width);
}
#main-outlet {
position: relative;
width: 95vw; // prevents content width shift during animation
&:after {
content: "";
background: rgba(0, 0, 0, 0.5);
position: absolute;
top: 0;
left: -2em; // compensate for gap
right: 0;
bottom: 0;
z-index: z("dropdown");
}
}
#main-outlet-wrapper {
grid-template-columns: min-content minmax(0, 100vw);
gap: 0 2em;
padding-left: 0;
.sidebar-container {
padding-bottom: 6.6em;
transition: width 0.25s;
}
}
}