diff --git a/core/src/main/resources/hudson/model/Run/console.jelly b/core/src/main/resources/hudson/model/Run/console.jelly index 34fa42286a8f..5e396d2b76f0 100644 --- a/core/src/main/resources/hudson/model/Run/console.jelly +++ b/core/src/main/resources/hudson/model/Run/console.jelly @@ -47,5 +47,7 @@ THE SOFTWARE. + + diff --git a/core/src/main/resources/lib/layout/backToTop.jelly b/core/src/main/resources/lib/layout/backToTop.jelly new file mode 100644 index 000000000000..bf13a65c4b6c --- /dev/null +++ b/core/src/main/resources/lib/layout/backToTop.jelly @@ -0,0 +1,9 @@ + + + + Adds a "back to top" floating button that appears when the user scrolls down. + + + diff --git a/core/src/main/resources/lib/layout/backToTop.properties b/core/src/main/resources/lib/layout/backToTop.properties new file mode 100644 index 000000000000..f86b1f96695b --- /dev/null +++ b/core/src/main/resources/lib/layout/backToTop.properties @@ -0,0 +1 @@ +Back\ to\ top=Back to top diff --git a/src/main/js/app.js b/src/main/js/app.js index d1ac7b26ca92..0d5020247987 100644 --- a/src/main/js/app.js +++ b/src/main/js/app.js @@ -7,6 +7,7 @@ import StopButtonLink from "@/components/stop-button-link"; import ConfirmationLink from "@/components/confirmation-link"; import Dialogs from "@/components/dialogs"; import Defer from "@/components/defer"; +import BackToTop from "@/components/back-to-top"; Dropdowns.init(); CommandPalette.init(); @@ -17,3 +18,4 @@ Tooltips.init(); StopButtonLink.init(); ConfirmationLink.init(); Dialogs.init(); +BackToTop.init(); diff --git a/src/main/js/components/back-to-top/index.js b/src/main/js/components/back-to-top/index.js new file mode 100644 index 000000000000..93654ff858e4 --- /dev/null +++ b/src/main/js/components/back-to-top/index.js @@ -0,0 +1,39 @@ +function init() { + const backToTopButton = document.querySelector(".jenkins-back-to-top"); + + if (!backToTopButton) { + return; + } + + const threshold = 300; + let ticking = false; + + const updateVisibility = () => { + if (window.scrollY > threshold) { + backToTopButton.classList.add("jenkins-back-to-top--visible"); + } else { + backToTopButton.classList.remove("jenkins-back-to-top--visible"); + } + ticking = false; + }; + + window.addEventListener( + "scroll", + () => { + if (!ticking) { + window.requestAnimationFrame(updateVisibility); + ticking = true; + } + }, + { passive: true }, + ); + + backToTopButton.addEventListener("click", () => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }); +} + +export default { init }; diff --git a/src/main/scss/components/_back-to-top.scss b/src/main/scss/components/_back-to-top.scss new file mode 100644 index 000000000000..992ffe1ef1b4 --- /dev/null +++ b/src/main/scss/components/_back-to-top.scss @@ -0,0 +1,43 @@ +@use "../abstracts/mixins"; + +.jenkins-back-to-top { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 900; + width: 2.75rem; + height: 2.75rem; + border-radius: 50%; + padding: 0; + box-shadow: var(--dropdown-box-shadow); + opacity: 0; + visibility: hidden; + transition: + opacity var(--standard-transition), + visibility var(--standard-transition), + transform var(--standard-transition), + background-color var(--standard-transition); + transform: translateY(0.625rem); + display: flex; + align-items: center; + justify-content: center; + + &--visible { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + + &:hover { + background-color: var(--button-background--hover); + } + + &:active { + background-color: var(--button-background--active); + } + + svg { + width: 1.25rem; + height: 1.25rem; + } +} diff --git a/src/main/scss/components/_index.scss b/src/main/scss/components/_index.scss index 03bd4c03cecf..7b427d77aeda 100644 --- a/src/main/scss/components/_index.scss +++ b/src/main/scss/components/_index.scss @@ -4,6 +4,7 @@ @use "badges"; @use "breadcrumbs"; @use "buttons"; +@use "back-to-top"; @use "cards"; @use "command-palette"; @use "content-blocks";