Update menu swipe (#13277)
* DEV: replace swipe events to use translate rather than left/right translate is better for animations. also use native css animations for opening and closing. * a11y: respect prefers reduced motion on mobile timeline * DEV: reduce jquery usage * DEV: add tests for menu swipe events test is run in 50% zoom/transform which means offsets and x of touch events need to be halved Refactor test window to use a transform rather than non-standard zoom property Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
This commit is contained in:
parent
1cd189aabf
commit
7c12ca89cb
|
@ -1,6 +1,5 @@
|
||||||
import PanEvents, {
|
import PanEvents, {
|
||||||
SWIPE_DISTANCE_THRESHOLD,
|
SWIPE_DISTANCE_THRESHOLD,
|
||||||
SWIPE_VELOCITY,
|
|
||||||
SWIPE_VELOCITY_THRESHOLD,
|
SWIPE_VELOCITY_THRESHOLD,
|
||||||
} from "discourse/mixins/pan-events";
|
} from "discourse/mixins/pan-events";
|
||||||
import { cancel, later, schedule } from "@ember/runloop";
|
import { cancel, later, schedule } from "@ember/runloop";
|
||||||
|
@ -23,7 +22,6 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
_isPanning: false,
|
_isPanning: false,
|
||||||
_panMenuOrigin: "right",
|
_panMenuOrigin: "right",
|
||||||
_panMenuOffset: 0,
|
_panMenuOffset: 0,
|
||||||
_scheduledMovingAnimation: null,
|
|
||||||
_scheduledRemoveAnimate: null,
|
_scheduledRemoveAnimate: null,
|
||||||
_topic: null,
|
_topic: null,
|
||||||
_mousetrap: null,
|
_mousetrap: null,
|
||||||
|
@ -37,26 +35,44 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
this.queueRerender();
|
this.queueRerender();
|
||||||
},
|
},
|
||||||
|
|
||||||
_animateOpening($panel) {
|
_animateOpening(panel) {
|
||||||
$panel.css({ right: "", left: "" });
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
|
panel.classList.add("animate");
|
||||||
|
headerCloak.classList.add("animate");
|
||||||
|
this._scheduledRemoveAnimate = later(() => {
|
||||||
|
panel.classList.remove("animate");
|
||||||
|
headerCloak.classList.remove("animate");
|
||||||
|
}, 200);
|
||||||
|
panel.style.setProperty("--offset", 0);
|
||||||
|
headerCloak.style.setProperty("--opacity", 0.5);
|
||||||
this._panMenuOffset = 0;
|
this._panMenuOffset = 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
_animateClosing($panel, menuOrigin, windowWidth) {
|
_animateClosing(panel, menuOrigin) {
|
||||||
$panel.css(menuOrigin, -windowWidth);
|
const windowWidth = document.body.offsetWidth;
|
||||||
this._animate = true;
|
this._animate = true;
|
||||||
schedule("afterRender", () => {
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
this.eventDispatched("dom:clean", "header");
|
panel.classList.add("animate");
|
||||||
this._panMenuOffset = 0;
|
headerCloak.classList.add("animate");
|
||||||
});
|
const offsetDirection = menuOrigin === "left" ? -1 : 1;
|
||||||
|
panel.style.setProperty("--offset", `${offsetDirection * windowWidth}px`);
|
||||||
|
headerCloak.style.setProperty("--opacity", 0);
|
||||||
|
this._scheduledRemoveAnimate = later(() => {
|
||||||
|
panel.classList.remove("animate");
|
||||||
|
headerCloak.classList.remove("animate");
|
||||||
|
schedule("afterRender", () => {
|
||||||
|
this.eventDispatched("dom:clean", "header");
|
||||||
|
this._panMenuOffset = 0;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
},
|
},
|
||||||
|
|
||||||
_isRTL() {
|
_isRTL() {
|
||||||
return $("html").css("direction") === "rtl";
|
return document.querySelector("html").classList["direction"] === "rtl";
|
||||||
},
|
},
|
||||||
|
|
||||||
_leftMenuClass() {
|
_leftMenuClass() {
|
||||||
return this._isRTL() ? ".user-menu" : ".hamburger-panel";
|
return this._isRTL() ? "user-menu" : "hamburger-panel";
|
||||||
},
|
},
|
||||||
|
|
||||||
_leftMenuAction() {
|
_leftMenuAction() {
|
||||||
|
@ -67,28 +83,14 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
return this._isRTL() ? "toggleHamburger" : "toggleUserMenu";
|
return this._isRTL() ? "toggleHamburger" : "toggleUserMenu";
|
||||||
},
|
},
|
||||||
|
|
||||||
_handlePanDone(offset, event) {
|
_handlePanDone(event) {
|
||||||
const $window = $(window);
|
const menuPanels = document.querySelectorAll(".menu-panel");
|
||||||
const windowWidth = $window.width();
|
|
||||||
const $menuPanels = $(".menu-panel");
|
|
||||||
const menuOrigin = this._panMenuOrigin;
|
const menuOrigin = this._panMenuOrigin;
|
||||||
this._shouldMenuClose(event, menuOrigin)
|
menuPanels.forEach((panel) => {
|
||||||
? (offset += SWIPE_VELOCITY)
|
if (this._shouldMenuClose(event, menuOrigin)) {
|
||||||
: (offset -= SWIPE_VELOCITY);
|
this._animateClosing(panel, menuOrigin);
|
||||||
$menuPanels.each((idx, panel) => {
|
|
||||||
const $panel = $(panel);
|
|
||||||
const $headerCloak = $(".header-cloak");
|
|
||||||
$panel.css(menuOrigin, -offset);
|
|
||||||
$headerCloak.css("opacity", Math.min(0.5, (300 - offset) / 600));
|
|
||||||
if (offset > windowWidth) {
|
|
||||||
this._animateClosing($panel, menuOrigin, windowWidth);
|
|
||||||
} else if (offset <= 0) {
|
|
||||||
this._animateOpening($panel);
|
|
||||||
} else {
|
} else {
|
||||||
//continue to open or close menu
|
this._animateOpening(panel);
|
||||||
this._scheduledMovingAnimation = window.requestAnimationFrame(() =>
|
|
||||||
this._handlePanDone(offset, event)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -114,11 +116,15 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
|
|
||||||
panStart(e) {
|
panStart(e) {
|
||||||
const center = e.center;
|
const center = e.center;
|
||||||
const $centeredElement = $(document.elementFromPoint(center.x, center.y));
|
const panOverValidElement = document
|
||||||
|
.elementsFromPoint(center.x, center.y)
|
||||||
|
.some(
|
||||||
|
(ele) =>
|
||||||
|
ele.classList.contains("panel-body") ||
|
||||||
|
ele.classList.contains("header-cloak")
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
($centeredElement.hasClass("panel-body") ||
|
panOverValidElement &&
|
||||||
$centeredElement.hasClass("header-cloak") ||
|
|
||||||
$centeredElement.parents(".panel-body").length) &&
|
|
||||||
(e.direction === "left" || e.direction === "right")
|
(e.direction === "left" || e.direction === "right")
|
||||||
) {
|
) {
|
||||||
e.originalEvent.preventDefault();
|
e.originalEvent.preventDefault();
|
||||||
|
@ -133,57 +139,51 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._isPanning = false;
|
this._isPanning = false;
|
||||||
$(".menu-panel").each((idx, panel) => {
|
this._handlePanDone(e);
|
||||||
const $panel = $(panel);
|
|
||||||
let offset = $panel.css("right");
|
|
||||||
if (this._panMenuOrigin === "left") {
|
|
||||||
offset = $panel.css("left");
|
|
||||||
}
|
|
||||||
offset = Math.abs(parseInt(offset, 10));
|
|
||||||
this._handlePanDone(offset, e);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
panMove(e) {
|
panMove(e) {
|
||||||
if (!this._isPanning) {
|
if (!this._isPanning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const $menuPanels = $(".menu-panel");
|
const panel = document.querySelector(".menu-panel");
|
||||||
$menuPanels.each((idx, panel) => {
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
const $panel = $(panel);
|
if (this._panMenuOrigin === "right") {
|
||||||
const $headerCloak = $(".header-cloak");
|
const pxClosed = Math.min(0, -e.deltaX + this._panMenuOffset);
|
||||||
if (this._panMenuOrigin === "right") {
|
panel.style.setProperty("--offset", `${-pxClosed}px`);
|
||||||
const pxClosed = Math.min(0, -e.deltaX + this._panMenuOffset);
|
headerCloak.style.setProperty(
|
||||||
$panel.css("right", pxClosed);
|
"--opacity",
|
||||||
$headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600));
|
Math.min(0.5, (300 + pxClosed) / 600)
|
||||||
} else {
|
);
|
||||||
const pxClosed = Math.min(0, e.deltaX + this._panMenuOffset);
|
} else {
|
||||||
$panel.css("left", pxClosed);
|
const pxClosed = Math.min(0, e.deltaX + this._panMenuOffset);
|
||||||
$headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600));
|
panel.style.setProperty("--offset", `${pxClosed}px`);
|
||||||
}
|
headerCloak.style.setProperty(
|
||||||
});
|
"--opacity",
|
||||||
|
Math.min(0.5, (300 + pxClosed) / 600)
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
dockCheck(info) {
|
dockCheck(info) {
|
||||||
const $header = $("header.d-header");
|
const header = document.querySelector("header.d-header");
|
||||||
|
|
||||||
if (this.docAt === null) {
|
if (this.docAt === null) {
|
||||||
if (!($header && $header.length === 1)) {
|
if (!header) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.docAt = $header.offset().top;
|
this.docAt = header.offsetTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $body = $("body");
|
|
||||||
const offset = info.offset();
|
const offset = info.offset();
|
||||||
if (offset >= this.docAt) {
|
if (offset >= this.docAt) {
|
||||||
if (!this.dockedHeader) {
|
if (!this.dockedHeader) {
|
||||||
$body.addClass("docked");
|
document.body.classList.add("docked");
|
||||||
this.dockedHeader = true;
|
this.dockedHeader = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.dockedHeader) {
|
if (this.dockedHeader) {
|
||||||
$body.removeClass("docked");
|
document.body.classList.remove("docked");
|
||||||
this.dockedHeader = false;
|
this.dockedHeader = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,13 +197,14 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
|
|
||||||
willRender() {
|
willRender() {
|
||||||
if (this.get("currentUser.staff")) {
|
if (this.get("currentUser.staff")) {
|
||||||
$("body").addClass("staff");
|
document.body.classList.add("staff");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
|
this._resizeDiscourseMenuPanel = () => this.afterRender();
|
||||||
|
window.addEventListener("resize", this._resizeDiscourseMenuPanel);
|
||||||
|
|
||||||
this.appEvents.on("header:show-topic", this, "setTopic");
|
this.appEvents.on("header:show-topic", this, "setTopic");
|
||||||
this.appEvents.on("header:hide-topic", this, "setTopic");
|
this.appEvents.on("header:hide-topic", this, "setTopic");
|
||||||
|
@ -279,14 +280,13 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
$(window).off("resize.discourse-menu-panel");
|
window.removeEventListener("resize", this._resizeDiscourseMenuPanel);
|
||||||
|
|
||||||
this.appEvents.off("header:show-topic", this, "setTopic");
|
this.appEvents.off("header:show-topic", this, "setTopic");
|
||||||
this.appEvents.off("header:hide-topic", this, "setTopic");
|
this.appEvents.off("header:hide-topic", this, "setTopic");
|
||||||
this.appEvents.off("dom:clean", this, "_cleanDom");
|
this.appEvents.off("dom:clean", this, "_cleanDom");
|
||||||
|
|
||||||
cancel(this._scheduledRemoveAnimate);
|
cancel(this._scheduledRemoveAnimate);
|
||||||
window.cancelAnimationFrame(this._scheduledMovingAnimation);
|
|
||||||
|
|
||||||
this._mousetrap.reset();
|
this._mousetrap.reset();
|
||||||
|
|
||||||
|
@ -308,25 +308,24 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $menuPanels = $(".menu-panel");
|
const menuPanels = document.querySelectorAll(".menu-panel");
|
||||||
if ($menuPanels.length === 0) {
|
if (menuPanels.length === 0) {
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
this._animate = true;
|
this._animate = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $window = $(window);
|
const windowWidth = document.body.offsetWidth;
|
||||||
const windowWidth = $window.width();
|
const headerWidth =
|
||||||
|
document.querySelector("#main-outlet .container").offsetWidth || 1100;
|
||||||
const headerWidth = $("#main-outlet .container").width() || 1100;
|
|
||||||
const remaining = (windowWidth - headerWidth) / 2;
|
const remaining = (windowWidth - headerWidth) / 2;
|
||||||
const viewMode = remaining < 50 ? "slide-in" : "drop-down";
|
const viewMode =
|
||||||
|
this.site.mobileView || remaining < 50 ? "slide-in" : "drop-down";
|
||||||
|
|
||||||
$menuPanels.each((idx, panel) => {
|
menuPanels.forEach((panel) => {
|
||||||
const $panel = $(panel);
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
const $headerCloak = $(".header-cloak");
|
let width = parseInt(panel.getAttribute("data-max-width"), 10) || 300;
|
||||||
let width = parseInt($panel.attr("data-max-width"), 10) || 300;
|
|
||||||
if (windowWidth - width < 50) {
|
if (windowWidth - width < 50) {
|
||||||
width = windowWidth - 50;
|
width = windowWidth - 50;
|
||||||
}
|
}
|
||||||
|
@ -334,51 +333,51 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
this._panMenuOffset = -width;
|
this._panMenuOffset = -width;
|
||||||
}
|
}
|
||||||
|
|
||||||
$panel.removeClass("drop-down slide-in").addClass(viewMode);
|
panel.classList.remove("drop-down");
|
||||||
|
panel.classList.remove("slide-in");
|
||||||
|
panel.classList.add(viewMode);
|
||||||
if (this._animate || this._panMenuOffset !== 0) {
|
if (this._animate || this._panMenuOffset !== 0) {
|
||||||
$headerCloak.css("opacity", 0);
|
|
||||||
if (
|
if (
|
||||||
this.site.mobileView &&
|
this.site.mobileView &&
|
||||||
$panel.parent(this._leftMenuClass()).length > 0
|
panel.parentElement.classList.contains(this._leftMenuClass())
|
||||||
) {
|
) {
|
||||||
this._panMenuOrigin = "left";
|
this._panMenuOrigin = "left";
|
||||||
$panel.css("left", -windowWidth);
|
panel.style.setProperty("--offset", `${-windowWidth}px`);
|
||||||
} else {
|
} else {
|
||||||
this._panMenuOrigin = "right";
|
this._panMenuOrigin = "right";
|
||||||
$panel.css("right", -windowWidth);
|
panel.style.setProperty("--offset", `${windowWidth}px`);
|
||||||
}
|
}
|
||||||
|
headerCloak.style.setProperty("--opacity", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $panelBody = $(".panel-body", $panel);
|
const panelBody = panel.querySelector(".panel-body");
|
||||||
|
|
||||||
// We use a mutationObserver to check for style changes, so it's important
|
// 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!
|
// we don't set it if it doesn't change. Same goes for the panelBody!
|
||||||
const style = $panel.prop("style");
|
|
||||||
|
|
||||||
if (viewMode === "drop-down") {
|
if (viewMode === "drop-down") {
|
||||||
const $buttonPanel = $("header ul.icons");
|
const buttonPanel = document.querySelectorAll("header ul.icons");
|
||||||
if ($buttonPanel.length === 0) {
|
if (buttonPanel.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These values need to be set here, not in the css file - this is to deal with the
|
// These values need to be set here, not in the css file - this is to deal with the
|
||||||
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
||||||
if (style.top !== "100%" || style.height !== "auto") {
|
if (panel.style.top !== "100%" || panel.style.height !== "auto") {
|
||||||
$panel.css({ top: "100%", height: "auto" });
|
panel.style.setProperty("top", "100%");
|
||||||
|
panel.style.setProperty("height", "auto");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("body").addClass("drop-down-mode");
|
document.body.classList.add("drop-down-mode");
|
||||||
} else {
|
} else {
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
$headerCloak.show();
|
headerCloak.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuTop = this.site.mobileView ? headerTop() : headerHeight();
|
const menuTop = this.site.mobileView ? headerTop() : headerHeight();
|
||||||
|
|
||||||
const winHeightOffset = 16;
|
const winHeightOffset = 16;
|
||||||
let initialWinHeight = window.innerHeight
|
let initialWinHeight = window.innerHeight;
|
||||||
? window.innerHeight
|
|
||||||
: $(window).height();
|
|
||||||
const winHeight = initialWinHeight - winHeightOffset;
|
const winHeight = initialWinHeight - winHeightOffset;
|
||||||
|
|
||||||
let height;
|
let height;
|
||||||
|
@ -394,27 +393,26 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
height = winHeight - menuTop - iPadOffset;
|
height = winHeight - menuTop - iPadOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($panelBody.prop("style").height !== "100%") {
|
if (panelBody.style.height !== "100%") {
|
||||||
$panelBody.height("100%");
|
panelBody.style.setProperty("height", "100%");
|
||||||
}
|
}
|
||||||
if (style.top !== menuTop + "px" || style[heightProp] !== height) {
|
if (
|
||||||
$panel.css({ top: menuTop + "px", [heightProp]: height });
|
panel.style.top !== `${menuTop}px` ||
|
||||||
$(".header-cloak").css({ top: menuTop + "px" });
|
panel.style[heightProp] !== `${height}px`
|
||||||
|
) {
|
||||||
|
panel.style.top = `${menuTop}px`;
|
||||||
|
panel.style.setProperty(heightProp, `${height}px`);
|
||||||
|
if (headerCloak) {
|
||||||
|
headerCloak.style.top = `${menuTop}px`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$("body").removeClass("drop-down-mode");
|
document.body.classList.remove("drop-down-mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
$panel.width(width);
|
panel.style.setProperty("width", `${width}px`);
|
||||||
if (this._animate) {
|
if (this._animate) {
|
||||||
$panel.addClass("animate");
|
this._animateOpening(panel);
|
||||||
$headerCloak.addClass("animate");
|
|
||||||
this._scheduledRemoveAnimate = later(() => {
|
|
||||||
$panel.removeClass("animate");
|
|
||||||
$headerCloak.removeClass("animate");
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
$panel.css({ right: "", left: "" });
|
|
||||||
$headerCloak.css("opacity", 0.5);
|
|
||||||
this._animate = false;
|
this._animate = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -426,21 +424,19 @@ export default SiteHeaderComponent.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
export function headerHeight() {
|
export function headerHeight() {
|
||||||
const $header = $("header.d-header");
|
const header = document.querySelector("header.d-header");
|
||||||
|
|
||||||
// Header may not exist in tests (e.g. in the user menu component test).
|
// Header may not exist in tests (e.g. in the user menu component test).
|
||||||
if ($header.length === 0) {
|
if (!header) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerOffset = $header.offset();
|
const headerOffsetTop = header.offsetTop ? header.offsetTop : 0;
|
||||||
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
|
return header.offsetHeight + headerOffsetTop - document.body.scrollTop;
|
||||||
return $header.outerHeight() + headerOffsetTop - $(window).scrollTop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function headerTop() {
|
export function headerTop() {
|
||||||
const $header = $("header.d-header");
|
const header = document.querySelector("header.d-header");
|
||||||
const headerOffset = $header.offset();
|
const headerOffsetTop = header.offsetTop ? header.offsetTop : 0;
|
||||||
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
|
return headerOffsetTop - document.body.scrollTop;
|
||||||
return headerOffsetTop - $(window).scrollTop();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import PanEvents, {
|
import PanEvents, {
|
||||||
SWIPE_DISTANCE_THRESHOLD,
|
SWIPE_DISTANCE_THRESHOLD,
|
||||||
SWIPE_VELOCITY,
|
|
||||||
SWIPE_VELOCITY_THRESHOLD,
|
SWIPE_VELOCITY_THRESHOLD,
|
||||||
} from "discourse/mixins/pan-events";
|
} from "discourse/mixins/pan-events";
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
|
@ -127,17 +126,18 @@ export default Component.extend(PanEvents, {
|
||||||
const $timelineContainer = $(".timeline-container");
|
const $timelineContainer = $(".timeline-container");
|
||||||
const maxOffset = parseInt($timelineContainer.css("height"), 10);
|
const maxOffset = parseInt($timelineContainer.css("height"), 10);
|
||||||
|
|
||||||
this._shouldPanClose(event)
|
$timelineContainer.addClass("animate");
|
||||||
? (offset += SWIPE_VELOCITY)
|
if (this._shouldPanClose(event)) {
|
||||||
: (offset -= SWIPE_VELOCITY);
|
$timelineContainer.css("--offset", `${maxOffset}px`);
|
||||||
|
later(() => {
|
||||||
$timelineContainer.css("bottom", -offset);
|
this._collapseFullscreen();
|
||||||
if (offset > maxOffset) {
|
$timelineContainer.removeClass("animate");
|
||||||
this._collapseFullscreen();
|
}, 200);
|
||||||
} else if (offset <= 0) {
|
|
||||||
$timelineContainer.css("bottom", "");
|
|
||||||
} else {
|
} else {
|
||||||
later(() => this._handlePanDone(offset, event), 20);
|
$timelineContainer.css("--offset", 0);
|
||||||
|
later(() => {
|
||||||
|
$timelineContainer.removeClass("animate");
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ export default Component.extend(PanEvents, {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.originalEvent.preventDefault();
|
e.originalEvent.preventDefault();
|
||||||
$(".timeline-container").css("bottom", Math.min(0, -e.deltaY));
|
$(".timeline-container").css("--offset", `${Math.max(0, e.deltaY)}px`);
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { cancel, later } from "@ember/runloop";
|
||||||
|
|
||||||
const helper = {
|
const helper = {
|
||||||
offset() {
|
offset() {
|
||||||
const mainOffset = $("#main").offset();
|
const main = document.querySelector("#main");
|
||||||
const offsetTop = mainOffset ? mainOffset.top : 0;
|
const offsetTop = main ? main.offsetTop : 0;
|
||||||
return (window.pageYOffset || $("html").scrollTop()) - offsetTop;
|
return window.pageYOffset - offsetTop;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,8 +32,8 @@ export default Mixin.create({
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
$(window).bind("scroll.discourse-dock", this.queueDockCheck);
|
window.addEventListener("scroll", this.queueDockCheck);
|
||||||
$(document).bind("touchmove.discourse-dock", this.queueDockCheck);
|
document.addEventListener("touchmove", this.queueDockCheck);
|
||||||
|
|
||||||
// dockCheck might happen too early on full page refresh
|
// dockCheck might happen too early on full page refresh
|
||||||
this._initialTimer = later(this, this.safeDockCheck, 50);
|
this._initialTimer = later(this, this.safeDockCheck, 50);
|
||||||
|
@ -47,7 +47,7 @@ export default Mixin.create({
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(this._initialTimer);
|
cancel(this._initialTimer);
|
||||||
$(window).unbind("scroll.discourse-dock", this.queueDockCheck);
|
window.removeEventListener("scroll", this.queueDockCheck);
|
||||||
$(document).unbind("touchmove.discourse-dock", this.queueDockCheck);
|
document.removeEventListener("touchmove", this.queueDockCheck);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,6 @@ import Mixin from "@ember/object/mixin";
|
||||||
Pan events is a mixin that allows components to detect and respond to swipe gestures
|
Pan events is a mixin that allows components to detect and respond to swipe gestures
|
||||||
It fires callbacks for panStart, panEnd, panMove with the pan state, and the original event.
|
It fires callbacks for panStart, panEnd, panMove with the pan state, and the original event.
|
||||||
**/
|
**/
|
||||||
export const SWIPE_VELOCITY = 40;
|
|
||||||
export const SWIPE_DISTANCE_THRESHOLD = 50;
|
export const SWIPE_DISTANCE_THRESHOLD = 50;
|
||||||
export const SWIPE_VELOCITY_THRESHOLD = 0.12;
|
export const SWIPE_VELOCITY_THRESHOLD = 0.12;
|
||||||
export const MINIMUM_SWIPE_DISTANCE = 5;
|
export const MINIMUM_SWIPE_DISTANCE = 5;
|
||||||
|
@ -14,35 +13,38 @@ export default Mixin.create({
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.addTouchListeners($(this.element));
|
this.addTouchListeners(this.element);
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.removeTouchListeners($(this.element));
|
this.removeTouchListeners(this.element);
|
||||||
},
|
},
|
||||||
|
|
||||||
addTouchListeners($element) {
|
addTouchListeners(element) {
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
$element
|
this.touchStart = (e) => e.touches && this._panStart(e.touches[0]);
|
||||||
.on("touchstart", (e) => e.touches && this._panStart(e.touches[0]))
|
this.touchMove = (e) => {
|
||||||
.on("touchmove", (e) => {
|
const touchEvent = e.touches[0];
|
||||||
const touchEvent = e.touches[0];
|
touchEvent.type = "pointermove";
|
||||||
touchEvent.type = "pointermove";
|
this._panMove(touchEvent, e);
|
||||||
this._panMove(touchEvent, e);
|
};
|
||||||
})
|
this.touchEnd = (e) => this._panMove({ type: "pointerup" }, e);
|
||||||
.on("touchend", (e) => this._panMove({ type: "pointerup" }, e))
|
this.touchCancel = (e) => this._panMove({ type: "pointercancel" }, e);
|
||||||
.on("touchcancel", (e) => this._panMove({ type: "pointercancel" }, e));
|
|
||||||
|
element.addEventListener("touchstart", this.touchStart);
|
||||||
|
element.addEventListener("touchmove", this.touchMove);
|
||||||
|
element.addEventListener("touchend", this.touchEnd);
|
||||||
|
element.addEventListener("touchcancel", this.touchCancel);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeTouchListeners($element) {
|
removeTouchListeners(element) {
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
$element
|
element.removeEventListener("touchstart", this.touchStart);
|
||||||
.off("touchstart")
|
element.removeEventListener("touchmove", this.touchMove);
|
||||||
.off("touchmove")
|
element.removeEventListener("touchend", this.touchEnd);
|
||||||
.off("touchend")
|
element.removeEventListener("touchcancel", this.touchCancel);
|
||||||
.off("touchcancel");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -366,22 +366,25 @@ export default createWidget("hamburger-menu", {
|
||||||
},
|
},
|
||||||
|
|
||||||
clickOutsideMobile(e) {
|
clickOutsideMobile(e) {
|
||||||
const $centeredElement = $(document.elementFromPoint(e.clientX, e.clientY));
|
const centeredElement = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
if (
|
const parents = document
|
||||||
$centeredElement.parents(".panel").length &&
|
.elementsFromPoint(e.clientX, e.clientY)
|
||||||
!$centeredElement.hasClass("header-cloak")
|
.some((ele) => ele.classList.contains("panel"));
|
||||||
) {
|
if (!centeredElement.classList.contains("header-cloak") && parents) {
|
||||||
this.sendWidgetAction("toggleHamburger");
|
this.sendWidgetAction("toggleHamburger");
|
||||||
} else {
|
} else {
|
||||||
const $window = $(window);
|
const windowWidth = document.body.offsetWidth;
|
||||||
const windowWidth = $window.width();
|
const panel = document.querySelector(".menu-panel");
|
||||||
const $panel = $(".menu-panel");
|
panel.classList.add("animate");
|
||||||
$panel.addClass("animate");
|
let offsetDirection = this.site.mobileView ? -1 : 1;
|
||||||
const panelOffsetDirection = this.site.mobileView ? "left" : "right";
|
offsetDirection =
|
||||||
$panel.css(panelOffsetDirection, -windowWidth);
|
document.querySelector("html").classList["direction"] === "rtl"
|
||||||
const $headerCloak = $(".header-cloak");
|
? -offsetDirection
|
||||||
$headerCloak.addClass("animate");
|
: offsetDirection;
|
||||||
$headerCloak.css("opacity", 0);
|
panel.style.setProperty("--offset", `${offsetDirection * windowWidth}px`);
|
||||||
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
|
headerCloak.classList.add("animate");
|
||||||
|
headerCloak.style.setProperty("--opacity", 0);
|
||||||
later(() => this.sendWidgetAction("toggleHamburger"), 200);
|
later(() => this.sendWidgetAction("toggleHamburger"), 200);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -265,21 +265,24 @@ export default createWidget("user-menu", {
|
||||||
},
|
},
|
||||||
|
|
||||||
clickOutsideMobile(e) {
|
clickOutsideMobile(e) {
|
||||||
const $centeredElement = $(document.elementFromPoint(e.clientX, e.clientY));
|
const centeredElement = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
if (
|
const parents = document
|
||||||
$centeredElement.parents(".panel").length &&
|
.elementsFromPoint(e.clientX, e.clientY)
|
||||||
!$centeredElement.hasClass("header-cloak")
|
.some((ele) => ele.classList.contains("panel"));
|
||||||
) {
|
if (!centeredElement.classList.contains("header-cloak") && parents) {
|
||||||
this.sendWidgetAction("toggleUserMenu");
|
this.sendWidgetAction("toggleUserMenu");
|
||||||
} else {
|
} else {
|
||||||
const $window = $(window);
|
const windowWidth = document.body.offsetWidth;
|
||||||
const windowWidth = $window.width();
|
const panel = document.querySelector(".menu-panel");
|
||||||
const $panel = $(".menu-panel");
|
panel.classList.add("animate");
|
||||||
$panel.addClass("animate");
|
let offsetDirection =
|
||||||
$panel.css("right", -windowWidth);
|
document.querySelector("html").classList["direction"] === "rtl"
|
||||||
const $headerCloak = $(".header-cloak");
|
? -1
|
||||||
$headerCloak.addClass("animate");
|
: 1;
|
||||||
$headerCloak.css("opacity", 0);
|
panel.style.setProperty("--offset", `${offsetDirection * windowWidth}px`);
|
||||||
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
|
headerCloak.classList.add("animate");
|
||||||
|
headerCloak.style.setProperty("--opacity", 0);
|
||||||
later(() => this.sendWidgetAction("toggleUserMenu"), 200);
|
later(() => this.sendWidgetAction("toggleUserMenu"), 200);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import { click, triggerEvent, visit } from "@ember/test-helpers";
|
||||||
|
import { test } from "qunit";
|
||||||
|
|
||||||
|
async function triggerSwipeStart(touchTarget) {
|
||||||
|
// some tests are shown in a zoom viewport.
|
||||||
|
// boundingClientRect is affected by the zoom and need to be multiplied by the zoom effect.
|
||||||
|
// EG: if the element has a zoom of 50%, this DOUBLES the x and y positions and offsets.
|
||||||
|
// The numbers you get from getBoundingClientRect are seen as twice as large... however, the
|
||||||
|
// touch input still deals with the base inputs, not doubled. This allows us to convert for those environments.
|
||||||
|
let zoom = parseFloat(
|
||||||
|
window.getComputedStyle(document.querySelector("#ember-testing")).zoom || 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Other tests are shown in a transformed viewport, and this is a multiple for the offsets
|
||||||
|
let scale = parseFloat(
|
||||||
|
window
|
||||||
|
.getComputedStyle(document.querySelector("#ember-testing"))
|
||||||
|
.transform.replace("matrix(", "") || 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const touchStart = {
|
||||||
|
touchTarget: touchTarget,
|
||||||
|
x:
|
||||||
|
zoom *
|
||||||
|
(touchTarget.getBoundingClientRect().x +
|
||||||
|
(scale * touchTarget.offsetWidth) / 2),
|
||||||
|
y:
|
||||||
|
zoom *
|
||||||
|
(touchTarget.getBoundingClientRect().y +
|
||||||
|
(scale * touchTarget.offsetHeight) / 2),
|
||||||
|
};
|
||||||
|
const touch = new Touch({
|
||||||
|
identifier: "test",
|
||||||
|
target: touchTarget,
|
||||||
|
clientX: touchStart.x,
|
||||||
|
clientY: touchStart.y,
|
||||||
|
});
|
||||||
|
await triggerEvent(touchTarget, "touchstart", {
|
||||||
|
touches: [touch],
|
||||||
|
targetTouches: [touch],
|
||||||
|
});
|
||||||
|
return touchStart;
|
||||||
|
}
|
||||||
|
async function triggerSwipeMove({ x, y, touchTarget }) {
|
||||||
|
const touch = new Touch({
|
||||||
|
identifier: "test",
|
||||||
|
target: touchTarget,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
});
|
||||||
|
await triggerEvent(touchTarget, "touchmove", {
|
||||||
|
touches: [touch],
|
||||||
|
targetTouches: [touch],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function triggerSwipeEnd({ x, y, touchTarget }) {
|
||||||
|
const touch = new Touch({
|
||||||
|
identifier: "test",
|
||||||
|
target: touchTarget,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
});
|
||||||
|
await triggerEvent(touchTarget, "touchend", {
|
||||||
|
touches: [touch],
|
||||||
|
targetTouches: [touch],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptance("Mobile - menu swipes", function (needs) {
|
||||||
|
needs.mobileView();
|
||||||
|
needs.user();
|
||||||
|
test("swipe to close hamburger", async function (assert) {
|
||||||
|
await visit("/");
|
||||||
|
await click(".hamburger-dropdown");
|
||||||
|
|
||||||
|
const touchTarget = document.querySelector(".panel-body");
|
||||||
|
let swipe = await triggerSwipeStart(touchTarget);
|
||||||
|
swipe.x -= 20;
|
||||||
|
await triggerSwipeMove(swipe);
|
||||||
|
await triggerSwipeEnd(swipe);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
queryAll(".panel-body").length === 0,
|
||||||
|
"it should close hamburger on a left swipe"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swipe back and flick to re-open hamburger", async function (assert) {
|
||||||
|
await visit("/");
|
||||||
|
await click(".hamburger-dropdown");
|
||||||
|
|
||||||
|
const touchTarget = document.querySelector(".panel-body");
|
||||||
|
let swipe = await triggerSwipeStart(touchTarget);
|
||||||
|
swipe.x -= 100;
|
||||||
|
await triggerSwipeMove(swipe);
|
||||||
|
swipe.x += 20;
|
||||||
|
await triggerSwipeMove(swipe);
|
||||||
|
await triggerSwipeEnd(swipe);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
queryAll(".panel-body").length === 1,
|
||||||
|
"it should re-open hamburger on a right swipe"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swipe to user menu", async function (assert) {
|
||||||
|
await visit("/");
|
||||||
|
await click("#current-user");
|
||||||
|
|
||||||
|
const touchTarget = document.querySelector(".panel-body");
|
||||||
|
let swipe = await triggerSwipeStart(touchTarget);
|
||||||
|
swipe.x += 20;
|
||||||
|
await triggerSwipeMove(swipe);
|
||||||
|
await triggerSwipeEnd(swipe);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
queryAll(".panel-body").length === 0,
|
||||||
|
"it should close user menu on a left swipe"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,7 +4,7 @@ document.write(
|
||||||
'<div id="ember-testing-container"><div id="ember-testing"></div></div>'
|
'<div id="ember-testing-container"><div id="ember-testing"></div></div>'
|
||||||
);
|
);
|
||||||
document.write(
|
document.write(
|
||||||
"<style>#ember-testing-container { position: fixed; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; transform: translateZ(0)} #ember-testing { zoom: 50%; }</style>"
|
"<style>#ember-testing-container { position: fixed; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; transform: translateZ(0)} #ember-testing { width: 200%; height: 200%; transform: scale(0.5); transform-origin: top left; }</style>"
|
||||||
);
|
);
|
||||||
|
|
||||||
let setupTestsLegacy = require("discourse/tests/setup-tests").setupTestsLegacy;
|
let setupTestsLegacy = require("discourse/tests/setup-tests").setupTestsLegacy;
|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
box-shadow: shadow("header");
|
box-shadow: shadow("header");
|
||||||
&.animate {
|
|
||||||
transition: right 0.2s ease-out, left 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -20,7 +20,9 @@
|
||||||
|
|
||||||
&.timeline-fullscreen.show {
|
&.timeline-fullscreen.show {
|
||||||
max-height: 700px;
|
max-height: 700px;
|
||||||
transition: max-height 0.4s ease-out;
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
transition: max-height 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-height: 425px) {
|
@media screen and (max-height: 425px) {
|
||||||
max-height: 75vh;
|
max-height: 75vh;
|
||||||
|
@ -37,8 +39,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.timeline-fullscreen {
|
&.timeline-fullscreen {
|
||||||
|
transform: translateY(var(--offset));
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
&.animate {
|
||||||
|
transition: transform 0.1s linear;
|
||||||
|
}
|
||||||
|
transition: max-height 0.3s ease-in;
|
||||||
|
}
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
transition: max-height 0.3s ease-in;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
|
|
|
@ -8,18 +8,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.header-cloak {
|
.header-cloak {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
opacity: 0.5;
|
--opacity: 0.5;
|
||||||
|
opacity: var(--opacity);
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: none;
|
display: none;
|
||||||
touch-action: pan-y pinch-zoom;
|
touch-action: pan-y pinch-zoom;
|
||||||
|
|
||||||
&.animate {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
transition: opacity 0.2s ease-out;
|
&.animate {
|
||||||
|
transition: opacity 0.1s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-panel.slide-in {
|
||||||
|
transform: translate(var(--offset), 0);
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
&.animate {
|
||||||
|
transition: transform 0.1s linear;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue