UI: improvements to footer nav for app and PWAs
Adds support for iPad and Chrome PWAs Better scroll direction logic when reaching bottom of the page
This commit is contained in:
parent
d21dd521d2
commit
f060c9b3ff
|
@ -5,14 +5,14 @@ import { observes } from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 150;
|
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 150;
|
||||||
|
|
||||||
const MobileFooterComponent = MountWidget.extend(
|
const FooterNavComponent = MountWidget.extend(
|
||||||
Scrolling,
|
Scrolling,
|
||||||
MobileScrollDirection,
|
MobileScrollDirection,
|
||||||
{
|
{
|
||||||
widget: "mobile-footer-nav",
|
widget: "footer-nav",
|
||||||
mobileScrollDirection: null,
|
mobileScrollDirection: null,
|
||||||
scrollEventDisabled: false,
|
scrollEventDisabled: false,
|
||||||
classNames: ["mobile-footer", "visible"],
|
classNames: ["footer-nav", "visible"],
|
||||||
routeHistory: [],
|
routeHistory: [],
|
||||||
currentRouteIndex: 0,
|
currentRouteIndex: 0,
|
||||||
canGoBack: false,
|
canGoBack: false,
|
||||||
|
@ -28,20 +28,22 @@ const MobileFooterComponent = MountWidget.extend(
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.bindScrolling({ name: "mobile-footer" });
|
this.bindScrolling({ name: "footer-nav" });
|
||||||
$(window).on("resize.mobile-footer-on-scroll", () => this.scrolled());
|
$(window).on("resize.footer-nav-on-scroll", () => this.scrolled());
|
||||||
this.appEvents.on("page:changed", this, "_routeChanged");
|
this.appEvents.on("page:changed", this, "_routeChanged");
|
||||||
this.appEvents.on("composer:opened", this, "_composerOpened");
|
this.appEvents.on("composer:opened", this, "_composerOpened");
|
||||||
this.appEvents.on("composer:closed", this, "_composerClosed");
|
this.appEvents.on("composer:closed", this, "_composerClosed");
|
||||||
|
$("body").addClass("with-footer-nav");
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.unbindScrolling("mobile-footer");
|
this.unbindScrolling("footer-nav");
|
||||||
$(window).unbind("resize.mobile-footer-on-scroll");
|
$(window).unbind("resize.footer-nav-on-scroll");
|
||||||
this.appEvents.off("page:changed", this, "_routeChanged");
|
this.appEvents.off("page:changed", this, "_routeChanged");
|
||||||
this.appEvents.off("composer:opened", this, "_composerOpened");
|
this.appEvents.off("composer:opened", this, "_composerOpened");
|
||||||
this.appEvents.off("composer:closed", this, "_composerClosed");
|
this.appEvents.off("composer:closed", this, "_composerClosed");
|
||||||
|
$("body").removeClass("with-footer-nav");
|
||||||
},
|
},
|
||||||
|
|
||||||
// The user has scrolled the window, or it is finished rendering and ready for processing.
|
// The user has scrolled the window, or it is finished rendering and ready for processing.
|
||||||
|
@ -75,7 +77,7 @@ const MobileFooterComponent = MountWidget.extend(
|
||||||
);
|
);
|
||||||
// body class used to adjust positioning of #topic-progress-wrapper
|
// body class used to adjust positioning of #topic-progress-wrapper
|
||||||
$("body").toggleClass(
|
$("body").toggleClass(
|
||||||
"mobile-footer-nav-visible",
|
"footer-nav-visible",
|
||||||
this.mobileScrollDirection === null ? true : false
|
this.mobileScrollDirection === null ? true : false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -119,10 +121,10 @@ const MobileFooterComponent = MountWidget.extend(
|
||||||
setBackForward() {
|
setBackForward() {
|
||||||
let index = this.get("currentRouteIndex");
|
let index = this.get("currentRouteIndex");
|
||||||
|
|
||||||
this.set("canGoBack", index > 1 ? true : false);
|
this.set("canGoBack", (index > 1 || document.referrer) ? true : false);
|
||||||
this.set("canGoForward", index < this.routeHistory.length ? true : false);
|
this.set("canGoForward", index < this.routeHistory.length ? true : false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default MobileFooterComponent;
|
export default FooterNavComponent;
|
|
@ -1,5 +1,5 @@
|
||||||
import computed from "ember-addons/ember-computed-decorators";
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
import { isAppWebview, isiOSPWA } from "discourse/lib/utilities";
|
import { isAppWebview, isiOSPWA, isChromePWA } from "discourse/lib/utilities";
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
showTop: true,
|
showTop: true,
|
||||||
|
@ -20,7 +20,11 @@ export default Ember.Controller.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
showMobileFooterNav() {
|
showFooterNav() {
|
||||||
return isAppWebview() || isiOSPWA();
|
return (
|
||||||
|
isAppWebview() ||
|
||||||
|
isiOSPWA() ||
|
||||||
|
(!this.site.isMobileDevice && isChromePWA())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -653,5 +653,14 @@ export function isiOSPWA() {
|
||||||
export function isAppWebview() {
|
export function isAppWebview() {
|
||||||
return window.ReactNativeWebView !== undefined;
|
return window.ReactNativeWebView !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isChromePWA() {
|
||||||
|
// Watch out: this doesn't distinguish between mobile or desktop PWAs
|
||||||
|
return (
|
||||||
|
window.matchMedia("(display-mode: standalone)").matches &&
|
||||||
|
navigator.userAgent.match(/(Chrome)/g)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// This prevents a mini racer crash
|
// This prevents a mini racer crash
|
||||||
export default {};
|
export default {};
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
const MOBILE_SCROLL_TOLERANCE = 5;
|
const MOBILE_SCROLL_TOLERANCE = 5;
|
||||||
|
|
||||||
export default Ember.Mixin.create({
|
export default Ember.Mixin.create({
|
||||||
_mobileLastScroll: null,
|
_lastScroll: null,
|
||||||
|
_bottomHit: 0,
|
||||||
|
|
||||||
calculateDirection(offset) {
|
calculateDirection(offset) {
|
||||||
// Difference between this scroll and the one before it.
|
// Difference between this scroll and the one before it.
|
||||||
const delta = Math.floor(offset - this._mobileLastScroll);
|
const delta = Math.floor(offset - this._lastScroll);
|
||||||
|
|
||||||
// This is a tiny scroll, so we ignore it.
|
// This is a tiny scroll, so we ignore it.
|
||||||
if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE)
|
if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE)
|
||||||
|
@ -15,26 +16,35 @@ export default Ember.Mixin.create({
|
||||||
const prevDirection = this.mobileScrollDirection;
|
const prevDirection = this.mobileScrollDirection;
|
||||||
const currDirection = delta > 0 ? "down" : null;
|
const currDirection = delta > 0 ? "down" : null;
|
||||||
|
|
||||||
// Handle Safari overscroll first
|
|
||||||
if (offset < 0) {
|
|
||||||
this.set("mobileScrollDirection", null);
|
|
||||||
} else if (currDirection !== prevDirection) {
|
|
||||||
this.set("mobileScrollDirection", currDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We store this to compare against it the next time the user scrolls
|
|
||||||
this._mobileLastScroll = Math.floor(offset);
|
|
||||||
|
|
||||||
// If the user reaches the very bottom of the topic, we want to reset the
|
|
||||||
// scroll direction in order for the header to switch back.
|
|
||||||
const distanceToBottom = Math.floor(
|
const distanceToBottom = Math.floor(
|
||||||
$("body").height() - offset - $(window).height()
|
$("body").height() - offset - $(window).height()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Not at the bottom yet
|
// Handle Safari top overscroll first
|
||||||
if (distanceToBottom > 0) return;
|
if (offset < 0) {
|
||||||
|
this.set("mobileScrollDirection", null);
|
||||||
|
} else if (currDirection !== prevDirection && distanceToBottom > 0) {
|
||||||
|
this.set("mobileScrollDirection", currDirection);
|
||||||
|
}
|
||||||
|
|
||||||
// We're at the bottom now, so we reset the direction.
|
// We store this to compare against it the next time the user scrolls
|
||||||
this.set("mobileScrollDirection", null);
|
this._lastScroll = Math.floor(offset);
|
||||||
|
|
||||||
|
// Not at the bottom yet
|
||||||
|
if (distanceToBottom > 0) {
|
||||||
|
this._bottomHit = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user reaches the very bottom of the topic, we only want to reset
|
||||||
|
// this scroll direction after a second scrolldown. This is a nicer event
|
||||||
|
// similar to what Safari and Chrome do.
|
||||||
|
Ember.run.debounce(() => {
|
||||||
|
this._bottomHit = 1;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
if (this._bottomHit === 1) {
|
||||||
|
this.set("mobileScrollDirection", null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,6 +34,6 @@
|
||||||
{{topic-entrance}}
|
{{topic-entrance}}
|
||||||
{{outlet "composer"}}
|
{{outlet "composer"}}
|
||||||
|
|
||||||
{{#if showMobileFooterNav}}
|
{{#if showFooterNav}}
|
||||||
{{mobile-footer}}
|
{{footer-nav}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { createWidget } from "discourse/widgets/widget";
|
import { createWidget } from "discourse/widgets/widget";
|
||||||
import { isAppWebview } from "discourse/lib/utilities";
|
import { isAppWebview, isChromePWA } from "discourse/lib/utilities";
|
||||||
|
|
||||||
createWidget("mobile-footer-nav", {
|
createWidget("footer-nav", {
|
||||||
tagName: "div.mobile-footer-nav",
|
tagName: "div.footer-nav-widget",
|
||||||
|
|
||||||
html(attrs) {
|
html(attrs) {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
@ -43,6 +43,15 @@ createWidget("mobile-footer-nav", {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isChromePWA()) {
|
||||||
|
buttons.push(
|
||||||
|
this.attach("flat-button", {
|
||||||
|
action: "refresh",
|
||||||
|
icon: "sync",
|
||||||
|
className: "btn-large"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
return buttons;
|
return buttons;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -54,5 +63,9 @@ createWidget("mobile-footer-nav", {
|
||||||
window.ReactNativeWebView.postMessage(
|
window.ReactNativeWebView.postMessage(
|
||||||
JSON.stringify({ shareUrl: window.location.href })
|
JSON.stringify({ shareUrl: window.location.href })
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -1,38 +1,42 @@
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// Mobile footer (displayed in DiscourseHub app and PWAs)
|
// Footer nav bar (displayed in DiscourseHub app and PWAs)
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
$footer-nav-height: 55px;
|
$footer-nav-height: 55px;
|
||||||
|
|
||||||
body.mobile-footer-nav-visible {
|
body.with-footer-nav {
|
||||||
padding-bottom: $footer-nav-height + 15;
|
padding-bottom: $footer-nav-height + 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.footer-nav-visible {
|
||||||
#topic-progress-wrapper,
|
#topic-progress-wrapper,
|
||||||
#reply-control.draft {
|
#reply-control.draft {
|
||||||
bottom: $footer-nav-height;
|
bottom: $footer-nav-height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-footer {
|
.footer-nav {
|
||||||
background-color: $header_background;
|
background-color: rgba($header_background, 0.9);
|
||||||
box-shadow: shadow("mobile-footer");
|
box-shadow: shadow("footer-nav");
|
||||||
height: $footer-nav-height;
|
height: $footer-nav-height;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: -$footer-nav-height;
|
bottom: -$footer-nav-height;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: z("mobile-footer");
|
z-index: z("footer-nav");
|
||||||
transition: all linear 0.15s;
|
transition: all linear 0.1s;
|
||||||
|
|
||||||
.d-icon {
|
.d-icon {
|
||||||
color: $header_primary-low-mid;
|
color: $header_primary-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.visible {
|
&.visible {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-footer-nav {
|
.footer-nav-widget {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: "space-evenly";
|
justify-content: "space-evenly";
|
||||||
@include unselectable;
|
@include unselectable;
|
||||||
|
@ -45,3 +49,11 @@ body.mobile-footer-nav-visible {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@supports (-webkit-backdrop-filter: blur(10px)) {
|
||||||
|
.footer-nav {
|
||||||
|
background-color: rgba($header_background, 0.7);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ $z-layers: (
|
||||||
"overlay": 1200
|
"overlay": 1200
|
||||||
),
|
),
|
||||||
"fullscreen": 1150,
|
"fullscreen": 1150,
|
||||||
"mobile-footer": 1140,
|
"footer-nav": 1140,
|
||||||
"mobile-composer": 1100,
|
"mobile-composer": 1100,
|
||||||
"header": 1000,
|
"header": 1000,
|
||||||
"tooltip": 600,
|
"tooltip": 600,
|
||||||
|
@ -130,7 +130,7 @@ $box-shadow: (
|
||||||
"card": 0 4px 14px rgba(0, 0, 0, 0.15),
|
"card": 0 4px 14px rgba(0, 0, 0, 0.15),
|
||||||
"dropdown": 0 2px 3px 0 rgba(0, 0, 0, 0.2),
|
"dropdown": 0 2px 3px 0 rgba(0, 0, 0, 0.2),
|
||||||
"header": 0 2px 4px -1px rgba(0, 0, 0, 0.25),
|
"header": 0 2px 4px -1px rgba(0, 0, 0, 0.25),
|
||||||
"mobile-footer": 0 2px 4px 1px rgba(0, 0, 0, 0.25),
|
"footer-nav": 0 2px 4px 1px rgba(0, 0, 0, 0.25),
|
||||||
"kbd": (
|
"kbd": (
|
||||||
0 2px 0 rgba(0, 0, 0, 0.2),
|
0 2px 0 rgba(0, 0, 0, 0.2),
|
||||||
0 0 0 1px dark-light-choose(#fff, #000) inset
|
0 0 0 1px dark-light-choose(#fff, #000) inset
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
@import "mobile/admin_report_counters";
|
@import "mobile/admin_report_counters";
|
||||||
@import "mobile/menu-panel";
|
@import "mobile/menu-panel";
|
||||||
@import "mobile/reviewables";
|
@import "mobile/reviewables";
|
||||||
@import "mobile/footer";
|
|
||||||
|
|
||||||
// Import all component-specific files
|
// Import all component-specific files
|
||||||
@import "mobile/components/*";
|
@import "mobile/components/*";
|
||||||
|
|
Loading…
Reference in New Issue