diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index eac53665d6f..b037a30003e 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -1,6 +1,5 @@ import PanEvents, { SWIPE_DISTANCE_THRESHOLD, - SWIPE_VELOCITY, SWIPE_VELOCITY_THRESHOLD, } from "discourse/mixins/pan-events"; import { cancel, later, schedule } from "@ember/runloop"; @@ -23,7 +22,6 @@ const SiteHeaderComponent = MountWidget.extend( _isPanning: false, _panMenuOrigin: "right", _panMenuOffset: 0, - _scheduledMovingAnimation: null, _scheduledRemoveAnimate: null, _topic: null, _mousetrap: null, @@ -37,26 +35,44 @@ const SiteHeaderComponent = MountWidget.extend( this.queueRerender(); }, - _animateOpening($panel) { - $panel.css({ right: "", left: "" }); + _animateOpening(panel) { + 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; }, - _animateClosing($panel, menuOrigin, windowWidth) { - $panel.css(menuOrigin, -windowWidth); + _animateClosing(panel, menuOrigin) { + const windowWidth = document.body.offsetWidth; this._animate = true; - schedule("afterRender", () => { - this.eventDispatched("dom:clean", "header"); - this._panMenuOffset = 0; - }); + const headerCloak = document.querySelector(".header-cloak"); + panel.classList.add("animate"); + 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() { - return $("html").css("direction") === "rtl"; + return document.querySelector("html").classList["direction"] === "rtl"; }, _leftMenuClass() { - return this._isRTL() ? ".user-menu" : ".hamburger-panel"; + return this._isRTL() ? "user-menu" : "hamburger-panel"; }, _leftMenuAction() { @@ -67,28 +83,14 @@ const SiteHeaderComponent = MountWidget.extend( return this._isRTL() ? "toggleHamburger" : "toggleUserMenu"; }, - _handlePanDone(offset, event) { - const $window = $(window); - const windowWidth = $window.width(); - const $menuPanels = $(".menu-panel"); + _handlePanDone(event) { + const menuPanels = document.querySelectorAll(".menu-panel"); const menuOrigin = this._panMenuOrigin; - this._shouldMenuClose(event, menuOrigin) - ? (offset += SWIPE_VELOCITY) - : (offset -= SWIPE_VELOCITY); - $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); + menuPanels.forEach((panel) => { + if (this._shouldMenuClose(event, menuOrigin)) { + this._animateClosing(panel, menuOrigin); } else { - //continue to open or close menu - this._scheduledMovingAnimation = window.requestAnimationFrame(() => - this._handlePanDone(offset, event) - ); + this._animateOpening(panel); } }); }, @@ -114,11 +116,15 @@ const SiteHeaderComponent = MountWidget.extend( panStart(e) { 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 ( - ($centeredElement.hasClass("panel-body") || - $centeredElement.hasClass("header-cloak") || - $centeredElement.parents(".panel-body").length) && + panOverValidElement && (e.direction === "left" || e.direction === "right") ) { e.originalEvent.preventDefault(); @@ -133,57 +139,51 @@ const SiteHeaderComponent = MountWidget.extend( return; } this._isPanning = false; - $(".menu-panel").each((idx, panel) => { - 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); - }); + this._handlePanDone(e); }, panMove(e) { if (!this._isPanning) { return; } - const $menuPanels = $(".menu-panel"); - $menuPanels.each((idx, panel) => { - const $panel = $(panel); - const $headerCloak = $(".header-cloak"); - if (this._panMenuOrigin === "right") { - const pxClosed = Math.min(0, -e.deltaX + this._panMenuOffset); - $panel.css("right", pxClosed); - $headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600)); - } else { - const pxClosed = Math.min(0, e.deltaX + this._panMenuOffset); - $panel.css("left", pxClosed); - $headerCloak.css("opacity", Math.min(0.5, (300 + pxClosed) / 600)); - } - }); + const panel = document.querySelector(".menu-panel"); + const headerCloak = document.querySelector(".header-cloak"); + if (this._panMenuOrigin === "right") { + const pxClosed = Math.min(0, -e.deltaX + this._panMenuOffset); + panel.style.setProperty("--offset", `${-pxClosed}px`); + headerCloak.style.setProperty( + "--opacity", + Math.min(0.5, (300 + pxClosed) / 600) + ); + } else { + const pxClosed = Math.min(0, e.deltaX + this._panMenuOffset); + panel.style.setProperty("--offset", `${pxClosed}px`); + headerCloak.style.setProperty( + "--opacity", + Math.min(0.5, (300 + pxClosed) / 600) + ); + } }, dockCheck(info) { - const $header = $("header.d-header"); + const header = document.querySelector("header.d-header"); if (this.docAt === null) { - if (!($header && $header.length === 1)) { + if (!header) { return; } - this.docAt = $header.offset().top; + this.docAt = header.offsetTop; } - const $body = $("body"); const offset = info.offset(); if (offset >= this.docAt) { if (!this.dockedHeader) { - $body.addClass("docked"); + document.body.classList.add("docked"); this.dockedHeader = true; } } else { if (this.dockedHeader) { - $body.removeClass("docked"); + document.body.classList.remove("docked"); this.dockedHeader = false; } } @@ -197,13 +197,14 @@ const SiteHeaderComponent = MountWidget.extend( willRender() { if (this.get("currentUser.staff")) { - $("body").addClass("staff"); + document.body.classList.add("staff"); } }, didInsertElement() { 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:hide-topic", this, "setTopic"); @@ -279,14 +280,13 @@ const SiteHeaderComponent = MountWidget.extend( willDestroyElement() { 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:hide-topic", this, "setTopic"); this.appEvents.off("dom:clean", this, "_cleanDom"); cancel(this._scheduledRemoveAnimate); - window.cancelAnimationFrame(this._scheduledMovingAnimation); this._mousetrap.reset(); @@ -308,25 +308,24 @@ const SiteHeaderComponent = MountWidget.extend( ); } - const $menuPanels = $(".menu-panel"); - if ($menuPanels.length === 0) { + const menuPanels = document.querySelectorAll(".menu-panel"); + if (menuPanels.length === 0) { if (this.site.mobileView) { this._animate = true; } return; } - const $window = $(window); - const windowWidth = $window.width(); - - const headerWidth = $("#main-outlet .container").width() || 1100; + const windowWidth = document.body.offsetWidth; + const headerWidth = + document.querySelector("#main-outlet .container").offsetWidth || 1100; 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) => { - const $panel = $(panel); - const $headerCloak = $(".header-cloak"); - let width = parseInt($panel.attr("data-max-width"), 10) || 300; + menuPanels.forEach((panel) => { + const headerCloak = document.querySelector(".header-cloak"); + let width = parseInt(panel.getAttribute("data-max-width"), 10) || 300; if (windowWidth - width < 50) { width = windowWidth - 50; } @@ -334,51 +333,51 @@ const SiteHeaderComponent = MountWidget.extend( 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) { - $headerCloak.css("opacity", 0); if ( this.site.mobileView && - $panel.parent(this._leftMenuClass()).length > 0 + panel.parentElement.classList.contains(this._leftMenuClass()) ) { this._panMenuOrigin = "left"; - $panel.css("left", -windowWidth); + panel.style.setProperty("--offset", `${-windowWidth}px`); } else { 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 don't set it if it doesn't change. Same goes for the $panelBody! - const style = $panel.prop("style"); + // we don't set it if it doesn't change. Same goes for the panelBody! if (viewMode === "drop-down") { - const $buttonPanel = $("header ul.icons"); - if ($buttonPanel.length === 0) { + const buttonPanel = document.querySelectorAll("header ul.icons"); + if (buttonPanel.length === 0) { return; } // 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. - if (style.top !== "100%" || style.height !== "auto") { - $panel.css({ top: "100%", height: "auto" }); + if (panel.style.top !== "100%" || panel.style.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 { if (this.site.mobileView) { - $headerCloak.show(); + headerCloak.style.display = "block"; } const menuTop = this.site.mobileView ? headerTop() : headerHeight(); const winHeightOffset = 16; - let initialWinHeight = window.innerHeight - ? window.innerHeight - : $(window).height(); + let initialWinHeight = window.innerHeight; const winHeight = initialWinHeight - winHeightOffset; let height; @@ -394,27 +393,26 @@ const SiteHeaderComponent = MountWidget.extend( height = winHeight - menuTop - iPadOffset; } - if ($panelBody.prop("style").height !== "100%") { - $panelBody.height("100%"); + if (panelBody.style.height !== "100%") { + panelBody.style.setProperty("height", "100%"); } - if (style.top !== menuTop + "px" || style[heightProp] !== height) { - $panel.css({ top: menuTop + "px", [heightProp]: height }); - $(".header-cloak").css({ top: menuTop + "px" }); + if ( + panel.style.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) { - $panel.addClass("animate"); - $headerCloak.addClass("animate"); - this._scheduledRemoveAnimate = later(() => { - $panel.removeClass("animate"); - $headerCloak.removeClass("animate"); - }, 200); + this._animateOpening(panel); } - $panel.css({ right: "", left: "" }); - $headerCloak.css("opacity", 0.5); this._animate = false; }); }, @@ -426,21 +424,19 @@ export default SiteHeaderComponent.extend({ }); 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). - if ($header.length === 0) { + if (!header) { return 0; } - const headerOffset = $header.offset(); - const headerOffsetTop = headerOffset ? headerOffset.top : 0; - return $header.outerHeight() + headerOffsetTop - $(window).scrollTop(); + const headerOffsetTop = header.offsetTop ? header.offsetTop : 0; + return header.offsetHeight + headerOffsetTop - document.body.scrollTop; } export function headerTop() { - const $header = $("header.d-header"); - const headerOffset = $header.offset(); - const headerOffsetTop = headerOffset ? headerOffset.top : 0; - return headerOffsetTop - $(window).scrollTop(); + const header = document.querySelector("header.d-header"); + const headerOffsetTop = header.offsetTop ? header.offsetTop : 0; + return headerOffsetTop - document.body.scrollTop; } diff --git a/app/assets/javascripts/discourse/app/components/topic-navigation.js b/app/assets/javascripts/discourse/app/components/topic-navigation.js index 5b58ff9147e..93d39a26602 100644 --- a/app/assets/javascripts/discourse/app/components/topic-navigation.js +++ b/app/assets/javascripts/discourse/app/components/topic-navigation.js @@ -1,6 +1,5 @@ import PanEvents, { SWIPE_DISTANCE_THRESHOLD, - SWIPE_VELOCITY, SWIPE_VELOCITY_THRESHOLD, } from "discourse/mixins/pan-events"; import Component from "@ember/component"; @@ -127,17 +126,18 @@ export default Component.extend(PanEvents, { const $timelineContainer = $(".timeline-container"); const maxOffset = parseInt($timelineContainer.css("height"), 10); - this._shouldPanClose(event) - ? (offset += SWIPE_VELOCITY) - : (offset -= SWIPE_VELOCITY); - - $timelineContainer.css("bottom", -offset); - if (offset > maxOffset) { - this._collapseFullscreen(); - } else if (offset <= 0) { - $timelineContainer.css("bottom", ""); + $timelineContainer.addClass("animate"); + if (this._shouldPanClose(event)) { + $timelineContainer.css("--offset", `${maxOffset}px`); + later(() => { + this._collapseFullscreen(); + $timelineContainer.removeClass("animate"); + }, 200); } 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; } e.originalEvent.preventDefault(); - $(".timeline-container").css("bottom", Math.min(0, -e.deltaY)); + $(".timeline-container").css("--offset", `${Math.max(0, e.deltaY)}px`); }, didInsertElement() { diff --git a/app/assets/javascripts/discourse/app/mixins/docking.js b/app/assets/javascripts/discourse/app/mixins/docking.js index 76ccd3a3f7d..e8de346cbc8 100644 --- a/app/assets/javascripts/discourse/app/mixins/docking.js +++ b/app/assets/javascripts/discourse/app/mixins/docking.js @@ -4,9 +4,9 @@ import { cancel, later } from "@ember/runloop"; const helper = { offset() { - const mainOffset = $("#main").offset(); - const offsetTop = mainOffset ? mainOffset.top : 0; - return (window.pageYOffset || $("html").scrollTop()) - offsetTop; + const main = document.querySelector("#main"); + const offsetTop = main ? main.offsetTop : 0; + return window.pageYOffset - offsetTop; }, }; @@ -32,8 +32,8 @@ export default Mixin.create({ didInsertElement() { this._super(...arguments); - $(window).bind("scroll.discourse-dock", this.queueDockCheck); - $(document).bind("touchmove.discourse-dock", this.queueDockCheck); + window.addEventListener("scroll", this.queueDockCheck); + document.addEventListener("touchmove", this.queueDockCheck); // dockCheck might happen too early on full page refresh this._initialTimer = later(this, this.safeDockCheck, 50); @@ -47,7 +47,7 @@ export default Mixin.create({ } cancel(this._initialTimer); - $(window).unbind("scroll.discourse-dock", this.queueDockCheck); - $(document).unbind("touchmove.discourse-dock", this.queueDockCheck); + window.removeEventListener("scroll", this.queueDockCheck); + document.removeEventListener("touchmove", this.queueDockCheck); }, }); diff --git a/app/assets/javascripts/discourse/app/mixins/pan-events.js b/app/assets/javascripts/discourse/app/mixins/pan-events.js index b1e1835d959..b40fd68eabd 100644 --- a/app/assets/javascripts/discourse/app/mixins/pan-events.js +++ b/app/assets/javascripts/discourse/app/mixins/pan-events.js @@ -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 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_VELOCITY_THRESHOLD = 0.12; export const MINIMUM_SWIPE_DISTANCE = 5; @@ -14,35 +13,38 @@ export default Mixin.create({ didInsertElement() { this._super(...arguments); - this.addTouchListeners($(this.element)); + this.addTouchListeners(this.element); }, willDestroyElement() { this._super(...arguments); - this.removeTouchListeners($(this.element)); + this.removeTouchListeners(this.element); }, - addTouchListeners($element) { + addTouchListeners(element) { if (this.site.mobileView) { - $element - .on("touchstart", (e) => e.touches && this._panStart(e.touches[0])) - .on("touchmove", (e) => { - const touchEvent = e.touches[0]; - touchEvent.type = "pointermove"; - this._panMove(touchEvent, e); - }) - .on("touchend", (e) => this._panMove({ type: "pointerup" }, e)) - .on("touchcancel", (e) => this._panMove({ type: "pointercancel" }, e)); + this.touchStart = (e) => e.touches && this._panStart(e.touches[0]); + this.touchMove = (e) => { + const touchEvent = e.touches[0]; + touchEvent.type = "pointermove"; + this._panMove(touchEvent, e); + }; + this.touchEnd = (e) => this._panMove({ type: "pointerup" }, e); + this.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) { - $element - .off("touchstart") - .off("touchmove") - .off("touchend") - .off("touchcancel"); + element.removeEventListener("touchstart", this.touchStart); + element.removeEventListener("touchmove", this.touchMove); + element.removeEventListener("touchend", this.touchEnd); + element.removeEventListener("touchcancel", this.touchCancel); } }, diff --git a/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js b/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js index ed042a99f33..1e65119e400 100644 --- a/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js @@ -366,22 +366,25 @@ export default createWidget("hamburger-menu", { }, clickOutsideMobile(e) { - const $centeredElement = $(document.elementFromPoint(e.clientX, e.clientY)); - if ( - $centeredElement.parents(".panel").length && - !$centeredElement.hasClass("header-cloak") - ) { + const centeredElement = document.elementFromPoint(e.clientX, e.clientY); + const parents = document + .elementsFromPoint(e.clientX, e.clientY) + .some((ele) => ele.classList.contains("panel")); + if (!centeredElement.classList.contains("header-cloak") && parents) { this.sendWidgetAction("toggleHamburger"); } else { - const $window = $(window); - const windowWidth = $window.width(); - const $panel = $(".menu-panel"); - $panel.addClass("animate"); - const panelOffsetDirection = this.site.mobileView ? "left" : "right"; - $panel.css(panelOffsetDirection, -windowWidth); - const $headerCloak = $(".header-cloak"); - $headerCloak.addClass("animate"); - $headerCloak.css("opacity", 0); + const windowWidth = document.body.offsetWidth; + const panel = document.querySelector(".menu-panel"); + panel.classList.add("animate"); + let offsetDirection = this.site.mobileView ? -1 : 1; + offsetDirection = + document.querySelector("html").classList["direction"] === "rtl" + ? -offsetDirection + : offsetDirection; + 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); } }, diff --git a/app/assets/javascripts/discourse/app/widgets/user-menu.js b/app/assets/javascripts/discourse/app/widgets/user-menu.js index 09cda2bb125..fb1de662335 100644 --- a/app/assets/javascripts/discourse/app/widgets/user-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/user-menu.js @@ -265,21 +265,24 @@ export default createWidget("user-menu", { }, clickOutsideMobile(e) { - const $centeredElement = $(document.elementFromPoint(e.clientX, e.clientY)); - if ( - $centeredElement.parents(".panel").length && - !$centeredElement.hasClass("header-cloak") - ) { + const centeredElement = document.elementFromPoint(e.clientX, e.clientY); + const parents = document + .elementsFromPoint(e.clientX, e.clientY) + .some((ele) => ele.classList.contains("panel")); + if (!centeredElement.classList.contains("header-cloak") && parents) { this.sendWidgetAction("toggleUserMenu"); } else { - const $window = $(window); - const windowWidth = $window.width(); - const $panel = $(".menu-panel"); - $panel.addClass("animate"); - $panel.css("right", -windowWidth); - const $headerCloak = $(".header-cloak"); - $headerCloak.addClass("animate"); - $headerCloak.css("opacity", 0); + const windowWidth = document.body.offsetWidth; + const panel = document.querySelector(".menu-panel"); + panel.classList.add("animate"); + let offsetDirection = + document.querySelector("html").classList["direction"] === "rtl" + ? -1 + : 1; + 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); } }, diff --git a/app/assets/javascripts/discourse/tests/acceptance/mobile-pan-test.js b/app/assets/javascripts/discourse/tests/acceptance/mobile-pan-test.js new file mode 100644 index 00000000000..837e303e80e --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/mobile-pan-test.js @@ -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" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/test_starter.js b/app/assets/javascripts/discourse/tests/test_starter.js index 77b69af2a10..8f75fcf8e5f 100644 --- a/app/assets/javascripts/discourse/tests/test_starter.js +++ b/app/assets/javascripts/discourse/tests/test_starter.js @@ -4,7 +4,7 @@ document.write( '
' ); document.write( - "" + "" ); let setupTestsLegacy = require("discourse/tests/setup-tests").setupTestsLegacy; diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 44e65feec06..83ef539616c 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -2,9 +2,6 @@ position: fixed; right: 0; box-shadow: shadow("header"); - &.animate { - transition: right 0.2s ease-out, left 0.2s ease-out; - } .panel-body { width: 100%; diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index df7d91f6bb1..a0ab83ceaca 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -20,7 +20,9 @@ &.timeline-fullscreen.show { 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) { max-height: 75vh; @@ -37,8 +39,14 @@ } &.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; - transition: max-height 0.3s ease-in; position: fixed; margin-left: 0; background-color: var(--secondary); diff --git a/app/assets/stylesheets/mobile/menu-panel.scss b/app/assets/stylesheets/mobile/menu-panel.scss index c2b0c527efe..e3351ef37dc 100644 --- a/app/assets/stylesheets/mobile/menu-panel.scss +++ b/app/assets/stylesheets/mobile/menu-panel.scss @@ -8,18 +8,30 @@ } } .header-cloak { - height: 100vh; - width: 100vw; + height: 100%; + width: 100%; position: fixed; background-color: black; - opacity: 0.5; + --opacity: 0.5; + opacity: var(--opacity); top: 0; left: 0; display: none; touch-action: pan-y pinch-zoom; - &.animate { - transition: opacity 0.2s ease-out; + @media (prefers-reduced-motion: no-preference) { + &.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; + } } }