From a3f2c15bc2c396bdc921650c5835d330adab3527 Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:13:51 +0000 Subject: [PATCH] Refine dialog component (#26033) * Init * push * Refine * Format * Refine * Update index.js --- .stylelintrc.js | 1 + .../resources/lib/form/bottomButtonBar.jelly | 6 ++- src/main/js/components/dialogs/index.js | 30 +++++++------ src/main/scss/abstracts/_theme.scss | 4 +- src/main/scss/components/_app-bar.scss | 45 +++++++++++++------ src/main/scss/components/_dialogs.scss | 44 +++++++++++------- .../main/webapp/scripts/hudson-behavior.js | 21 ++++++--- 7 files changed, 98 insertions(+), 53 deletions(-) diff --git a/.stylelintrc.js b/.stylelintrc.js index 50820909fb..765545dc64 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -17,5 +17,6 @@ module.exports = { "number-max-precision": 5, "no-duplicate-selectors": null, "hue-degree-notation": "number", + "scss/operator-no-newline-after": null, }, }; diff --git a/core/src/main/resources/lib/form/bottomButtonBar.jelly b/core/src/main/resources/lib/form/bottomButtonBar.jelly index dab0895e29..2bab23e302 100644 --- a/core/src/main/resources/lib/form/bottomButtonBar.jelly +++ b/core/src/main/resources/lib/form/bottomButtonBar.jelly @@ -28,9 +28,13 @@ THE SOFTWARE. Creates a button bar at the bottom of the page for things like "Submit". The actual buttons should be specified as the body of this tag. This area will always be visible at the bottom of the screen. + + + Hides the border of the bottom app bar unless its overlaying content. + -
+
diff --git a/src/main/js/components/dialogs/index.js b/src/main/js/components/dialogs/index.js index 60282e4100..c8be1000a8 100644 --- a/src/main/js/components/dialogs/index.js +++ b/src/main/js/components/dialogs/index.js @@ -13,6 +13,7 @@ let _defaults = { hideCloseButton: false, allowEmpty: false, submitButton: false, + preventCloseOnOutsideClick: false, }; let _typeClassMap = { @@ -39,11 +40,12 @@ Dialog.prototype.init = function () { this.dialog.style.minWidth = this.options.minWidth; document.body.appendChild(this.dialog); - if (this.options.title != null) { - const title = createElementFromHtml(`
`); - this.dialog.appendChild(title); - title.innerText = this.options.title; - } + // Append title element + const title = createElementFromHtml( + `
`, + ); + this.dialog.appendChild(title); + title.querySelector("span").innerText = this.options.title; if (this.dialogType === "modal") { if (this.options.content != null) { @@ -55,22 +57,24 @@ Dialog.prototype.init = function () { } if (this.options.hideCloseButton !== true) { const closeButton = createElementFromHtml(` - `); - this.dialog.appendChild(closeButton); + title.append(closeButton); closeButton.addEventListener("click", () => this.dialog.dispatchEvent(new Event("cancel")), ); } - this.dialog.addEventListener("click", function (e) { - if (e.target !== e.currentTarget) { - return; - } - this.dispatchEvent(new Event("cancel")); - }); + if (!this.options.preventCloseOnOutsideClick) { + this.dialog.addEventListener("click", function (e) { + if (e.target !== e.currentTarget) { + return; + } + this.dispatchEvent(new Event("cancel")); + }); + } this.ok = null; } else { this.form = null; diff --git a/src/main/scss/abstracts/_theme.scss b/src/main/scss/abstracts/_theme.scss index 0c15085fc0..5fa0ace494 100644 --- a/src/main/scss/abstracts/_theme.scss +++ b/src/main/scss/abstracts/_theme.scss @@ -77,6 +77,7 @@ $semantics: ( --header-height: 4.125rem; // App bar + --bottom-app-bar-padding: 0.875rem; --bottom-app-bar-shadow: color-mix( in sRGB, var(--text-color-secondary) 8%, @@ -201,8 +202,7 @@ $semantics: ( --dialog-background: var(--card-background); --dialog-box-shadow: var(--jenkins-border--subtle-shadow), 0 5px 25px rgb(0 0 10 / 0.025), - 0 0 1.5px color-mix(in sRGB, var(--black) 20%, transparent), - inset 0 0 0 1px rgb(255 255 255 / 0.05); + 0 0 1.5px color-mix(in sRGB, var(--black) 20%, transparent); ::backdrop { --dialog-backdrop-backdrop-filter: blur(0.25px); diff --git a/src/main/scss/components/_app-bar.scss b/src/main/scss/components/_app-bar.scss index b6921afddb..ec6b35a389 100644 --- a/src/main/scss/components/_app-bar.scss +++ b/src/main/scss/components/_app-bar.scss @@ -125,25 +125,16 @@ text-overflow: ellipsis; } -$bottom-app-bar-padding: 0.875rem; - .jenkins-bottom-app-bar__shadow { - --semi-translucent: color-mix(in sRGB, var(--background) 75%, transparent); + --semi-translucent: color-mix(in sRGB, var(--background) 70%, transparent); position: sticky; height: calc( - 2.375rem + ($bottom-app-bar-padding * 2) + 1px + var(--jenkins-border-width) + 2.375rem + (var(--bottom-app-bar-padding) * 2) + 1px + + var(--jenkins-border-width) ); bottom: -1px; - margin-bottom: calc((2.375rem + $bottom-app-bar-padding) * -1); - backdrop-filter: blur(20px); - background: linear-gradient( - to right, - var(--background), - var(--semi-translucent) 3rem, - var(--semi-translucent) calc(100% - 3rem), - var(--background) - ); + margin-bottom: calc((2.375rem + var(--bottom-app-bar-padding)) * -1); border-top: var(--jenkins-border-width) solid var(--jenkins-border-color); z-index: 997; @@ -167,7 +158,33 @@ $bottom-app-bar-padding: 0.875rem; opacity: 0; } + &--borderless:not(&--stuck) { + border-top-color: transparent; + } + + &--borderless { + margin-top: calc( + (var(--bottom-app-bar-padding) + var(--jenkins-border-width)) * -1 + ); + } + &--stuck { + &::before { + content: ""; + position: absolute; + inset: 0; + backdrop-filter: blur(20px); + } + + background: linear-gradient( + to right, + var(--background), + var(--background) 3rem, + var(--semi-translucent), + var(--background) calc(100% - 3rem), + var(--background) + ); + &::after { opacity: 1 !important; } @@ -183,5 +200,5 @@ $bottom-app-bar-padding: 0.875rem; .bottom-sticker-inner { position: relative; - padding: $bottom-app-bar-padding 0; + padding: var(--bottom-app-bar-padding) 0; } diff --git a/src/main/scss/components/_dialogs.scss b/src/main/scss/components/_dialogs.scss index c955da7e76..90b29a776d 100644 --- a/src/main/scss/components/_dialogs.scss +++ b/src/main/scss/components/_dialogs.scss @@ -2,6 +2,8 @@ $jenkins-dialog-padding: 1.25rem; .jenkins-dialog { --background: var(--dialog-background); + --section-padding: #{$jenkins-dialog-padding}; + --bottom-app-bar-padding: #{$jenkins-dialog-padding}; border-radius: 1rem; border: none; @@ -9,11 +11,12 @@ $jenkins-dialog-padding: 1.25rem; box-shadow: var(--dialog-box-shadow); animation: jenkins-dialog-animate-in 0.25s cubic-bezier(0, 0.68, 0.5, 1.5); overflow: hidden; - padding: $jenkins-dialog-padding 0 0 0; + padding: 0; display: flex; flex-direction: column; - gap: $jenkins-dialog-padding; - outline: none; + + // Thin highlight for dark themes to differentiate the dialog from its backdrop + outline: 1px solid rgb(255 255 255 / 0.05); &::backdrop { background: color-mix( @@ -26,13 +29,26 @@ $jenkins-dialog-padding: 1.25rem; } &__title { + display: flex; + gap: 1rem; + align-items: center; font-size: 1rem; font-weight: var(--font-bold-weight); - padding: 0 $jenkins-dialog-padding; + padding: $jenkins-dialog-padding; color: var(--text-color); overflow-wrap: anywhere; text-box: cap alphabetic; - margin-top: 0.5625rem; + + &__button { + padding: 0; + width: 2rem; + min-height: 2rem; + border-radius: 100%; + } + + &__close-button { + margin-left: auto !important; + } } &__contents { @@ -62,7 +78,7 @@ $jenkins-dialog-padding: 1.25rem; &__buttons { display: flex; - padding: 0 $jenkins-dialog-padding $jenkins-dialog-padding; + padding: $jenkins-dialog-padding; justify-content: right; flex-direction: row-reverse; gap: 1.25rem; @@ -80,16 +96,6 @@ $jenkins-dialog-padding: 1.25rem; margin: 0 0 1rem; } - &__close-button { - position: absolute; - top: $jenkins-dialog-padding; - right: $jenkins-dialog-padding; - padding: 0; - width: 2rem; - min-height: 2rem; - border-radius: 100%; - } - &[closing] { animation: jenkins-dialog-animate-out 0.1s linear; @@ -97,6 +103,12 @@ $jenkins-dialog-padding: 1.25rem; animation: jenkins-dialog-backdrop-animate-out 0.1s linear; } } + + .jenkins-bottom-app-bar__shadow { + margin-bottom: calc( + (2.375rem + calc(var(--bottom-app-bar-padding) * 2)) * -1 + ); + } } @keyframes jenkins-dialog-backdrop-animate-in { diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js index f2614c66f6..21602c5975 100644 --- a/war/src/main/webapp/scripts/hudson-behavior.js +++ b/war/src/main/webapp/scripts/hudson-behavior.js @@ -1781,22 +1781,29 @@ function rowvgStartEachRow(recursive, f) { }, ); - window.addEventListener("load", function () { - // Add a class to the bottom bar when it's stuck to the bottom of the screen - const el = document.querySelector(".jenkins-bottom-app-bar__shadow"); - if (el) { + // Add a class to the bottom bar when it's stuck to the bottom of the screen + Behaviour.specify( + ".jenkins-bottom-app-bar__shadow", + "jenkins-bottom-app-bar__shadow", + 0, + function (el) { + const dialog = el.closest("dialog"); + const observer = new IntersectionObserver( ([e]) => e.target.classList.toggle( "jenkins-bottom-app-bar__shadow--stuck", e.intersectionRatio < 1, ), - { threshold: [1] }, + { + threshold: [1], + root: dialog || null, + }, ); observer.observe(el); - } - }); + }, + ); /** * Function that provides compatibility to the checkboxes without title on an f:entry