This is an automated email from the ASF dual-hosted git repository.

reiern70 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 6848a4fd598098619bb4177fb7dd6851f51ef78a
Author: reiern70 <[email protected]>
AuthorDate: Fri Oct 2 09:57:57 2026 -0500

    Fix the Ajax veil for replaced, scrolled and positioned hosts and redirects
    
    Review of the veil behaviors (GitHub issue #1631) found cases where the
    veil did not hold, or disturbed the page it covers:
    
    - A host replaced while still veiled, by a pushed progress update or by
      a request on another channel, lost its veil until the request ended.
      The veil now listens to /dom/node/added and moves onto the new element
      at once.
    - On an Ajax redirect /ajax/call/done still fired, so the veil came down
      and a second click could send the request again before the next page
      arrived. Core keeps its activity indicator up in that case, but done
      subscribers could not tell. Both Ajax engines now pass whether the
      call is redirecting as an extra argument of /ajax/call/done, and the
      veil stays up until the page is left. It is lowered when the page is
      restored from the back-forward cache.
    - Wicket.Veil.hide() shared one counter with Ajax requests, so a hide
      whose show never arrived lowered the veil of a running request. Server
      raises are counted on their own now.
    - The veil was chosen from attrs.c, which for a delegated behavior is
      the container, not the element clicked. It now starts from the event
      target, as core does, and falls back to attrs.c when that element is
      gone.
    - Local veils were kept in a plain object, so a markup id such as
      "constructor" resolved to an Object member. They are kept in a Map.
    - In a scrolling host the veil scrolled away with the content. It is now
      kept over the visible part while the host scrolls.
    - The host was forced to position: relative, so an absolute, fixed or
      sticky host jumped on every request. Only a static host gets it now,
      through the new class wicket-veil-host-static. wicket-veil-host isolates
      the host instead, so the veil no longer paints over a sticky header or
      a modal dialog in front of it.
    - The options script was formatted with the default locale, which can
      localize digits and break the script. It uses Locale.ROOT.
    
    An application subscribing to /ajax/call/done sees no difference unless
    its callback reads the new argument, a boolean that follows attrs.
    Wicket.Ajax.Call.done() takes it as an optional second parameter.
---
 .../wicket/ajax/res/js/wicket-ajax-jquery.js       |   6 +-
 .../org/apache/wicket/ajax/res/js/wicket-ajax.js   |   6 +-
 wicket-core/src/test/js/ajax.js                    |  40 +++++
 .../extensions/ajax/veil/AbstractVeilBehavior.java |  13 +-
 .../extensions/ajax/veil/LocalVeilBehavior.java    |  11 +-
 .../wicket/extensions/ajax/veil/wicket-veil.css    |   4 +
 .../wicket/extensions/ajax/veil/wicket-veil.js     | 133 +++++++++++-----
 .../extensions/ajax/veil/VeilBehaviorTest.java     |  20 +++
 wicket-extensions/src/test/js/veil-test.js         | 169 ++++++++++++++++++++-
 wicket-extensions/src/test/js/veil.html            |   1 +
 .../src/main/asciidoc/ajax/ajax_12.adoc            |   9 +-
 .../src/main/asciidoc/ajax/ajax_6.adoc             |   2 +-
 12 files changed, 358 insertions(+), 56 deletions(-)

diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js 
b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
index 288dcd004c..11540e767e 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
+++ 
b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
@@ -742,7 +742,7 @@
                                                
self._executeHandlers(attrs.coh, attrs, jqXHR, textStatus);
                                                
we.publish(topic.AJAX_CALL_COMPLETE, attrs, jqXHR, textStatus);
 
-                                               self.done(attrs);
+                                               self.done(attrs, 
context.isRedirecting === true);
                                                return FunctionsExecuter.DONE;
                                        }, self));
 
@@ -929,9 +929,9 @@
                        }, this));
                },
 
-               done: function (attrs) {
+               done: function (attrs, isRedirecting) {
                        this._executeHandlers(attrs.dh, attrs);
-                       Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_DONE, 
attrs);
+                       Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_DONE, 
attrs, isRedirecting === true);
 
                        Wicket.channelManager.done(attrs.ch);
                },
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js 
b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js
index befb85810a..d24e0a5f20 100644
--- a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js
+++ b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js
@@ -956,7 +956,7 @@
                                                
self._executeHandlers(attrs.coh, attrs, xhr, textStatus);
                                                
we.publish(topic.AJAX_CALL_COMPLETE, attrs, xhr, textStatus);
 
-                                               self.done(attrs);
+                                               self.done(attrs, 
context.isRedirecting === true);
                                                return FunctionsExecuter.DONE;
                                        }).bind(self));
 
@@ -1143,9 +1143,9 @@
                        }).bind(this));
                },
 
-               done: function (attrs) {
+               done: function (attrs, isRedirecting) {
                        this._executeHandlers(attrs.dh, attrs);
-                       Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_DONE, 
attrs);
+                       Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_DONE, 
attrs, isRedirecting === true);
 
                        Wicket.channelManager.done(attrs.ch);
                },
diff --git a/wicket-core/src/test/js/ajax.js b/wicket-core/src/test/js/ajax.js
index 8a173f6358..1937e3bbc4 100644
--- a/wicket-core/src/test/js/ajax.js
+++ b/wicket-core/src/test/js/ajax.js
@@ -1327,6 +1327,46 @@ Wicket.Event.add(window, 'domready', function() {
                        Wicket.Event.remove(window, "event1");
                });
 
+               test('The done topic tells its subscribers that the response 
redirects.', assert => {
+                       const done = assert.async();
+                       assert.expect(1);
+
+                       var oldRedirect = Wicket.Ajax.redirect;
+                       Wicket.Ajax.redirect = function() {};
+
+                       Wicket.Event.subscribe('/ajax/call/done', 
function(jqEvent, attributes, isRedirecting) {
+                               assert.strictEqual(isRedirecting, true, 'Done: 
isRedirecting');
+                               Wicket.Event.unsubscribe();
+                               Wicket.Ajax.redirect = oldRedirect;
+                               done();
+                       });
+
+                       Wicket.Ajax.ajax({
+                               u: 'data/ajax/redirectAjaxResponse.xml',
+                               e: 'event1'
+                       });
+                       Wicket.Event.fire(window, "event1");
+                       Wicket.Event.remove(window, "event1");
+               });
+
+               test('The done topic tells its subscribers that the response 
does not redirect.', assert => {
+                       const done = assert.async();
+                       assert.expect(1);
+
+                       Wicket.Event.subscribe('/ajax/call/done', 
function(jqEvent, attributes, isRedirecting) {
+                               assert.strictEqual(isRedirecting, false, 'Done: 
isRedirecting');
+                               Wicket.Event.unsubscribe();
+                               done();
+                       });
+
+                       Wicket.Ajax.ajax({
+                               u: 'data/ajax/emptyAjaxResponse.xml',
+                               e: 'event1'
+                       });
+                       Wicket.Event.fire(window, "event1");
+                       Wicket.Event.remove(window, "event1");
+               });
+
                test('processAjaxResponse, normal HTTP case.', assert => {
                        const done = assert.async();
                        assert.expect(2);
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/AbstractVeilBehavior.java
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/AbstractVeilBehavior.java
index 1acf840c5a..5f197d0e23 100644
--- 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/AbstractVeilBehavior.java
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/AbstractVeilBehavior.java
@@ -17,6 +17,7 @@
 package org.apache.wicket.extensions.ajax.veil;
 
 import java.time.Duration;
+import java.util.Locale;
 
 import org.apache.wicket.Component;
 import org.apache.wicket.behavior.Behavior;
@@ -43,8 +44,14 @@ import org.apache.wicket.util.lang.Args;
  * alike, and can be changed per behavior with {@link 
#setSpinnerDelay(Duration)} and
  * {@link #setMinimumSpinnerTime(Duration)}. The veil does not intercept the 
keyboard.
  * <p>
+ * When the response redirects the browser to another page, the veil stays up 
until the browser
+ * leaves the page, so the request cannot be sent again meanwhile. A redirect 
that does not leave
+ * the page, such as one to a file download, leaves the veil up too; offer 
downloads through
+ * {@link org.apache.wicket.extensions.ajax.AjaxDownloadBehavior} instead.
+ * <p>
  * The look comes from {@code wicket-veil.css} and can be overridden with the 
classes
- * {@code wicket-veil}, {@code wicket-veil-busy} and {@code wicket-veil-host}.
+ * {@code wicket-veil}, {@code wicket-veil-busy}, {@code wicket-veil-host} and
+ * {@code wicket-veil-host-static}.
  * <p>
  * A request is left unveiled when it carries the extra parameter
  * {@value PageVeilBehavior#NO_VEIL_PARAMETER}, see {@link 
PageVeilBehavior#noVeil}.
@@ -144,7 +151,7 @@ public abstract class AbstractVeilBehavior extends Behavior
         */
        protected final String getOptions()
        {
-               return String.format("{\"delay\":%d,\"minimum\":%d}", 
getSpinnerDelay().toMillis(),
-                       getMinimumSpinnerTime().toMillis());
+               return String.format(Locale.ROOT, 
"{\"delay\":%d,\"minimum\":%d}",
+                       getSpinnerDelay().toMillis(), 
getMinimumSpinnerTime().toMillis());
        }
 }
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/LocalVeilBehavior.java
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/LocalVeilBehavior.java
index cbd0854aa8..197c96e381 100644
--- 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/LocalVeilBehavior.java
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/LocalVeilBehavior.java
@@ -30,10 +30,13 @@ import com.github.openjson.JSONObject;
  * page, even when it has a {@link PageVeilBehavior}. Requests fired from 
elsewhere leave the
  * component alone.
  * <p>
- * The veil is appended to the component's element, which gets the class
- * {@code wicket-veil-host} with {@code position: relative} for the duration, 
so the component
- * has to render an element that can hold a {@code div}. The behavior makes 
the component output
- * its markup id.
+ * The veil is appended to the component's element, so the component has to 
render an element
+ * that can hold a {@code div}. For the duration, the element gets the class
+ * {@code wicket-veil-host}, which isolates it so the veil stays within its 
stacking context, and,
+ * unless it is positioned already, {@code wicket-veil-host-static} with
+ * {@code position: relative}. When the element scrolls, the veil covers its 
visible part. If an
+ * Ajax update or a push replaces the element while it is veiled, the veil 
moves onto the new
+ * element. The behavior makes the component output its markup id.
  * <p>
  * The server can raise the veil too, for an update it is about to push to the 
component, for
  * example through a WebSocket connection: send {@link #getVeilMessage()} as a 
text message when
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.css
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.css
index 6736ec668e..c545ca40d1 100644
--- 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.css
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.css
@@ -27,6 +27,10 @@
 }
 
 .wicket-veil-host {
+       isolation: isolate;
+}
+
+.wicket-veil-host-static {
        position: relative;
 }
 
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.js
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.js
index c207dfe3f9..718c433dbb 100644
--- 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.js
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.js
@@ -21,7 +21,8 @@
  * The veil is transparent and only blocks the mouse. If a request is still 
running after the
  * target's spinner delay, the veil gets the 'wicket-veil-busy' class, which 
shows a spinner;
  * once shown, the spinner stays for at least the target's minimum time, so it 
does not flicker.
- * A request carrying the extra parameter 'wicket_nb' is never veiled.
+ * A request carrying the extra parameter 'wicket_nb' is never veiled. A 
request whose response
+ * redirects the browser keeps its veil until the page is left.
  *
  * A local veil can also be raised by the server, for a component it is about 
to update through a
  * WebSocket push: a WebSocket text message {"wicketVeil":"show","id":"<markup 
id>"} raises it,
@@ -38,25 +39,42 @@
        const VEIL_CLASS = 'wicket-veil';
        const BUSY_CLASS = 'wicket-veil-busy';
        const HOST_CLASS = 'wicket-veil-host';
+       const STATIC_HOST_CLASS = 'wicket-veil-host-static';
        const WEBSOCKET_MESSAGE_TOPIC = '/websocket/message';
        const MESSAGE_PREFIX = '{"wicketVeil"';
 
        let pageTarget = null;
-       let localTargets = {};
+       const localTargets = new Map();
        let subscribed = false;
 
+       // the veil scrolls with the content of its host, so it is moved back 
over the visible part
+       function follow(target) {
+               const style = target.veil.style;
+               const top = target.host.scrollTop;
+               const left = target.host.scrollLeft;
+               style.top = top ? top + 'px' : '';
+               style.bottom = top ? -top + 'px' : '';
+               style.left = left ? left + 'px' : '';
+               style.right = left ? -left + 'px' : '';
+       }
+
        function createTarget(id, options) {
-               return {
+               const target = {
                        id: id,
                        delay: options.delay,
                        minimum: options.minimum,
                        count: 0,
+                       raised: 0,
                        host: null,
                        veil: null,
                        shownAt: -1,
                        spinnerTimer: null,
                        hideTimer: null
                };
+               target.onScroll = function () {
+                       follow(target);
+               };
+               return target;
        }
 
        function configure(target, options) {
@@ -76,24 +94,43 @@
        }
 
        function findTarget(attrs) {
-               let node = attrs.c ? document.getElementById(attrs.c) : null;
+               let node = attrs.event && attrs.event.target;
+               if (!node || !node.isConnected) {
+                       node = typeof(attrs.c) === "string" ? 
document.getElementById(attrs.c) : null;
+               }
                for (; node && node !== document; node = node.parentNode) {
-                       if (node.id && localTargets[node.id]) {
-                               return localTargets[node.id];
+                       const target = node.id && localTargets.get(node.id);
+                       if (target) {
+                               return target;
                        }
                }
                return pageTarget;
        }
 
        function dropStaleTargets() {
-               for (const id in localTargets) {
-                       if (Object.prototype.hasOwnProperty.call(localTargets, 
id) &&
-                               localTargets[id].count === 0 && 
!document.getElementById(id)) {
-                               delete localTargets[id];
+               for (const [id, target] of localTargets) {
+                       if (target.count === 0 && !document.getElementById(id)) 
{
+                               localTargets.delete(id);
                        }
                }
        }
 
+       function attach(target, host) {
+               host.classList.add(HOST_CLASS);
+               if (getComputedStyle(host).position === 'static') {
+                       host.classList.add(STATIC_HOST_CLASS);
+               }
+               host.appendChild(target.veil);
+               host.addEventListener('scroll', target.onScroll);
+               target.host = host;
+               follow(target);
+       }
+
+       function detach(target) {
+               target.host.classList.remove(HOST_CLASS, STATIC_HOST_CLASS);
+               target.host.removeEventListener('scroll', target.onScroll);
+       }
+
        function hide(target) {
                const clock = Wicket.Veil._clock;
                clock.clearTimeout(target.spinnerTimer);
@@ -105,7 +142,7 @@
                        target.veil.parentNode.removeChild(target.veil);
                }
                if (target.host && target !== pageTarget) {
-                       target.host.classList.remove(HOST_CLASS);
+                       detach(target);
                }
                target.veil = null;
                target.host = null;
@@ -123,15 +160,15 @@
                        hide(target);
                }
 
-               const host = target === pageTarget ? document.body : 
document.getElementById(target.id);
                const veil = document.createElement('div');
                veil.className = VEIL_CLASS;
-               if (target !== pageTarget) {
-                       host.classList.add(HOST_CLASS);
-               }
-               host.appendChild(veil);
-               target.host = host;
                target.veil = veil;
+               if (target === pageTarget) {
+                       document.body.appendChild(veil);
+                       target.host = document.body;
+               } else {
+                       attach(target, document.getElementById(target.id));
+               }
 
                target.spinnerTimer = clock.setTimeout(function () {
                        target.spinnerTimer = null;
@@ -146,9 +183,8 @@
                }
                const host = document.getElementById(target.id);
                if (host) {
-                       host.classList.add(HOST_CLASS);
-                       host.appendChild(target.veil);
-                       target.host = host;
+                       detach(target);
+                       attach(target, host);
                }
        }
 
@@ -186,6 +222,18 @@
                }
        }
 
+       function lowerAll() {
+               const targets = Array.from(localTargets.values());
+               if (pageTarget !== null) {
+                       targets.push(pageTarget);
+               }
+               targets.forEach(function (target) {
+                       target.count = 0;
+                       target.raised = 0;
+                       hide(target);
+               });
+       }
+
        function onBeforeSend(jqEvent, attrs) {
                if (!attrs || isOptedOut(attrs)) {
                        return;
@@ -199,15 +247,28 @@
                acquire(target);
        }
 
-       function onDone(jqEvent, attrs) {
+       function onDone(jqEvent, attrs, isRedirecting) {
                const target = attrs && attrs.wicketVeil;
-               if (!target) {
+               if (!target || isRedirecting === true) {
                        return;
                }
                delete attrs.wicketVeil;
                releaseOne(target);
        }
 
+       function onDomNodeAdded() {
+               for (const target of localTargets.values()) {
+                       reattach(target);
+               }
+       }
+
+       function onPageShow(event) {
+               // a page restored from the back-forward cache may still carry 
the veil of a redirect
+               if (event.persisted) {
+                       lowerAll();
+               }
+       }
+
        function onWebSocketMessage(jqEvent, message) {
                if (typeof(message) !== "string" || 
message.indexOf(MESSAGE_PREFIX) !== 0) {
                        return;
@@ -230,7 +291,9 @@
                        subscribed = true;
                        
Wicket.Event.subscribe(Wicket.Event.Topic.AJAX_CALL_BEFORE_SEND, onBeforeSend);
                        
Wicket.Event.subscribe(Wicket.Event.Topic.AJAX_CALL_DONE, onDone);
+                       
Wicket.Event.subscribe(Wicket.Event.Topic.DOM_NODE_ADDED, onDomNodeAdded);
                        Wicket.Event.subscribe(WEBSOCKET_MESSAGE_TOPIC, 
onWebSocketMessage);
+                       window.addEventListener('pageshow', onPageShow);
                }
        }
 
@@ -260,11 +323,11 @@
                 */
                local: function (id, options) {
                        subscribe();
-                       const target = localTargets[id];
+                       const target = localTargets.get(id);
                        if (target) {
                                configure(target, options);
                        } else {
-                               localTargets[id] = createTarget(id, options);
+                               localTargets.set(id, createTarget(id, options));
                        }
                },
 
@@ -275,21 +338,24 @@
                 * @param id {String} - the markup id of a component with a 
local veil
                 */
                show: function (id) {
-                       const target = localTargets[id];
+                       const target = localTargets.get(id);
                        if (target && document.getElementById(id)) {
+                               target.raised++;
                                acquire(target);
                        }
                },
 
                /**
                 * Lowers the local veil raised by show(), respecting the 
spinner's minimum time. Calls
-                * without a matching show() are ignored.
+                * without a matching show() are ignored, and leave the veil of 
a running Ajax request
+                * alone.
                 *
                 * @param id {String} - the markup id of a component with a 
local veil
                 */
                hide: function (id) {
-                       const target = localTargets[id];
-                       if (target) {
+                       const target = localTargets.get(id);
+                       if (target && target.raised > 0) {
+                               target.raised--;
                                releaseOne(target);
                        }
                },
@@ -309,16 +375,9 @@
                },
 
                _reset: function () {
-                       if (pageTarget !== null) {
-                               hide(pageTarget);
-                       }
-                       for (const id in localTargets) {
-                               if 
(Object.prototype.hasOwnProperty.call(localTargets, id)) {
-                                       hide(localTargets[id]);
-                               }
-                       }
+                       lowerAll();
                        pageTarget = null;
-                       localTargets = {};
+                       localTargets.clear();
                }
        };
 })();
diff --git 
a/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/veil/VeilBehaviorTest.java
 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/veil/VeilBehaviorTest.java
index f714e49d2f..d4fba2d224 100644
--- 
a/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/veil/VeilBehaviorTest.java
+++ 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/veil/VeilBehaviorTest.java
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.time.Duration;
+import java.util.Locale;
 
 import org.apache.wicket.MarkupContainer;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -105,6 +106,25 @@ class VeilBehaviorTest extends WicketTestCase
                        "\", {\"delay\":0,\"minimum\":2000});"), response);
        }
 
+       @Test
+       void timingsDoNotDependOnTheDefaultLocale()
+       {
+               Locale defaultLocale = Locale.getDefault();
+               Locale.setDefault(Locale.forLanguageTag("th-TH-u-nu-thai"));
+               try
+               {
+                       tester.startPage(new TestPage(new PageVeilBehavior(), 
null, false));
+               }
+               finally
+               {
+                       Locale.setDefault(defaultLocale);
+               }
+
+               String response = tester.getLastResponseAsString();
+               
assertTrue(response.contains("Wicket.Veil.page({\"delay\":300,\"minimum\":500});"),
+                       response);
+       }
+
        @Test
        void negativeTimingsAreRejected()
        {
diff --git a/wicket-extensions/src/test/js/veil-test.js 
b/wicket-extensions/src/test/js/veil-test.js
index 57e342d822..7b9cc6a5fd 100644
--- a/wicket-extensions/src/test/js/veil-test.js
+++ b/wicket-extensions/src/test/js/veil-test.js
@@ -71,8 +71,8 @@ Wicket.Event.add(window, 'domready', function() {
                return attrs;
        }
 
-       function done(attrs) {
-               Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_DONE, attrs);
+       function done(attrs, isRedirecting) {
+               Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_DONE, attrs, 
isRedirecting === true);
        }
 
        function push(message) {
@@ -239,6 +239,66 @@ Wicket.Event.add(window, 'domready', function() {
                done(attrs);
                assert.equal(veils().length, 0, "the local veil stayed after 
the request finished");
                assert.notOk(outer.classList.contains('wicket-veil-host'), "the 
host class stayed");
+               
assert.notOk(outer.classList.contains('wicket-veil-host-static'), "the static 
host class stayed");
+       });
+
+       test("a static host is positioned and isolated while it is veiled", 
assert => {
+               Wicket.Veil.local('veilOuter', OPTIONS);
+               const outer = document.getElementById('veilOuter');
+
+               const attrs = send({ c: 'veilOuterLink' });
+               assert.ok(outer.classList.contains('wicket-veil-host-static'), 
"the static host is not marked");
+               assert.equal(getComputedStyle(outer).position, 'relative', "the 
static host is not positioned");
+               assert.equal(getComputedStyle(outer).isolation, 'isolate', "the 
host is not isolated");
+
+               done(attrs);
+               assert.equal(getComputedStyle(outer).position, 'static', "the 
host stayed positioned");
+       });
+
+       test("a host positioned by a stylesheet rule keeps its position", 
assert => {
+               const rule = document.createElement('style');
+               rule.textContent = '.veil-test-absolute { position: absolute; 
}';
+               document.head.insertBefore(rule, document.head.firstChild);
+               Wicket.Veil.local('veilOuter', OPTIONS);
+               const outer = document.getElementById('veilOuter');
+               outer.classList.add('veil-test-absolute');
+
+               const attrs = send({ c: 'veilOuterLink' });
+               
assert.notOk(outer.classList.contains('wicket-veil-host-static'), "the 
positioned host was marked static");
+               assert.equal(getComputedStyle(outer).position, 'absolute', "the 
host lost its position");
+
+               done(attrs);
+               rule.remove();
+       });
+
+       test("a local veil covers the visible part of a scrolled host", assert 
=> {
+               Wicket.Veil.local('veilOuter', OPTIONS);
+               const outer = document.getElementById('veilOuter');
+               outer.style.height = '40px';
+               outer.style.overflow = 'auto';
+               const filler = document.createElement('div');
+               filler.style.height = '400px';
+               outer.appendChild(filler);
+               outer.scrollTop = 100;
+
+               const attrs = send({ c: 'veilOuterLink' });
+               const veil = veilOf(outer);
+               const covers = function () {
+                       const veilBox = veil.getBoundingClientRect();
+                       const hostBox = outer.getBoundingClientRect();
+                       return Math.round(veilBox.top) === 
Math.round(hostBox.top + outer.clientTop) &&
+                               Math.round(veilBox.height) === 
outer.clientHeight;
+               };
+               assert.ok(covers(), "the veil does not cover the visible part 
of the host");
+
+               outer.scrollTop = 150;
+               outer.dispatchEvent(new Event('scroll'));
+               assert.ok(covers(), "the veil did not follow the host's 
scrolling");
+
+               done(attrs);
+               outer.scrollTop = 0;
+               outer.dispatchEvent(new Event('scroll'));
+               assert.equal(veils().length, 0, "the local veil stayed after 
the request finished");
        });
 
        test("a local veil shows the spinner after its delay and keeps it for 
its minimum time", assert => {
@@ -293,6 +353,44 @@ Wicket.Event.add(window, 'domready', function() {
                done(attrs);
        });
 
+       test("a delegated request is taken by the innermost veil around the 
element that was clicked", assert => {
+               Wicket.Veil.page(OPTIONS);
+               Wicket.Veil.local('veilOuter', OPTIONS);
+               Wicket.Veil.local('veilInner', OPTIONS);
+
+               const attrs = send({ c: 'veilOuter', event: { target: 
document.getElementById('veilInnerLink') } });
+               assert.ok(veilOf(document.getElementById('veilInner')), "the 
veil around the clicked element was not raised");
+               assert.equal(veils().length, 1, "more than the innermost veil 
was raised");
+
+               done(attrs);
+       });
+
+       test("a request whose clicked element is gone is taken by the veil 
around its component", assert => {
+               Wicket.Veil.local('veilOuter', OPTIONS);
+               Wicket.Veil.local('veilInner', OPTIONS);
+
+               const attrs = send({ c: 'veilOuterLink', event: { target: 
document.createElement('a') } });
+               assert.ok(veilOf(document.getElementById('veilOuter')), "the 
component's veil was not raised");
+               assert.equal(veils().length, 1, "more than the component's veil 
was raised");
+
+               done(attrs);
+       });
+
+       test("markup ids naming Object members do not break the veils", assert 
=> {
+               Wicket.Veil.page(OPTIONS);
+               Wicket.Veil.local('constructor', OPTIONS);
+               const link = document.createElement('a');
+               link.id = 'toString';
+               document.getElementById('qunit-fixture').appendChild(link);
+
+               const attrs = send({ c: 'toString' });
+               assert.ok(veilOf(document.body), "the page was not veiled");
+               assert.strictEqual(Object.delay, undefined, "the options were 
written onto Object");
+
+               done(attrs);
+               assert.equal(veils().length, 0, "the veil stayed after the 
request finished");
+       });
+
        test("a request from outside a local veil falls back to the page veil", 
assert => {
                Wicket.Veil.page(OPTIONS);
                Wicket.Veil.local('veilInner', OPTIONS);
@@ -370,6 +468,73 @@ Wicket.Event.add(window, 'domready', function() {
                
assert.notOk(replacement.classList.contains('wicket-veil-host'), "the host 
class stayed");
        });
 
+       test("a veil whose component is replaced during the request moves onto 
the new element at once", assert => {
+               Wicket.Veil.local('veilOuter', OPTIONS);
+
+               const attrs = send({ c: 'veilOuterLink' });
+               clock.tick(300);
+               Wicket.DOM.replace(document.getElementById('veilOuter'),
+                       '<div id="veilOuter"><a id="veilOuterLink" 
href="#outer">outer</a></div>');
+               const replacement = document.getElementById('veilOuter');
+
+               const veil = veilOf(replacement);
+               assert.ok(veil, "the veil did not move onto the new element");
+               assert.ok(isBusy(veil), "the moved veil lost its spinner");
+               assert.ok(replacement.classList.contains('wicket-veil-host'), 
"the new element is not marked as host");
+
+               done(attrs);
+               clock.tick(500);
+               assert.equal(veils().length, 0, "the veil stayed beyond the 
spinner's minimum time");
+       });
+
+       test("a pushed veil follows its component when a progress update 
replaces it", assert => {
+               Wicket.Veil.local('veilOuter', OPTIONS);
+
+               push('{"wicketVeil":"show","id":"veilOuter"}');
+               Wicket.DOM.replace(document.getElementById('veilOuter'),
+                       '<div id="veilOuter"><a id="veilOuterLink" 
href="#outer">outer</a></div>');
+               clock.tick(300);
+
+               const veil = veilOf(document.getElementById('veilOuter'));
+               assert.ok(veil, "the veil did not move onto the new element");
+               assert.ok(isBusy(veil), "the spinner was not shown on the moved 
veil");
+
+               Wicket.Veil.hide('veilOuter');
+               clock.tick(500);
+               assert.equal(veils().length, 0, "the veil stayed after it was 
hidden");
+       });
+
+       test("a response redirecting the browser keeps the veil until the page 
is left", assert => {
+               Wicket.Veil.page(OPTIONS);
+
+               const attrs = send({ c: 'veilPageLink' });
+               done(attrs, true);
+               clock.tick(1000);
+               assert.ok(veilOf(document.body), "the veil came down although 
the browser is leaving the page");
+               assert.ok(isBusy(veilOf(document.body)), "the spinner did not 
show while the browser is leaving the page");
+
+               window.dispatchEvent(new window.PageTransitionEvent('pageshow', 
{ persisted: true }));
+               assert.equal(veils().length, 0, "the veil stayed on the page 
restored from the back-forward cache");
+
+               const next = send({ c: 'veilPageLink' });
+               assert.ok(veilOf(document.body), "the next request was not 
veiled");
+               done(next);
+               assert.equal(veils().length, 0, "the veil stayed after the next 
request finished");
+       });
+
+       test("an unmatched hide leaves the veil of a running request alone", 
assert => {
+               Wicket.Veil.local('veilOuter', OPTIONS);
+               const outer = document.getElementById('veilOuter');
+
+               const attrs = send({ c: 'veilOuterLink' });
+               Wicket.Veil.hide('veilOuter');
+               push('{"wicketVeil":"hide","id":"veilOuter"}');
+               assert.ok(veilOf(outer), "an unmatched hide lowered the veil of 
a running request");
+
+               done(attrs);
+               assert.equal(veils().length, 0, "the veil stayed after the 
request finished");
+       });
+
        test("other WebSocket messages, unknown ids and unmatched hides are 
ignored", assert => {
                Wicket.Veil.page(OPTIONS);
                Wicket.Veil.local('veilOuter', OPTIONS);
diff --git a/wicket-extensions/src/test/js/veil.html 
b/wicket-extensions/src/test/js/veil.html
index f9deee8f02..4510d13148 100644
--- a/wicket-extensions/src/test/js/veil.html
+++ b/wicket-extensions/src/test/js/veil.html
@@ -21,6 +21,7 @@
        <title id="titleId">Wicket.Veil JavaScript tests</title>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" href="/wicket-core/src/test/js/qunit/qunit.css" 
type="text/css" media="screen" />
+       <link rel="stylesheet" 
href="/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/veil/wicket-veil.css"
 type="text/css" />
 </head>
 
 <body>
diff --git a/wicket-user-guide/src/main/asciidoc/ajax/ajax_12.adoc 
b/wicket-user-guide/src/main/asciidoc/ajax/ajax_12.adoc
index 74f9383789..baec6c6e92 100644
--- a/wicket-user-guide/src/main/asciidoc/ajax/ajax_12.adoc
+++ b/wicket-user-guide/src/main/asciidoc/ajax/ajax_12.adoc
@@ -18,6 +18,8 @@ public class BasePage extends WebPage {
 
 Adding it to any other component throws an _IllegalArgumentException_.
 
+When the response redirects the browser to another page, for example after 
_setResponsePage()_, the veil stays up until the browser leaves the page, so a 
second click cannot send the request again in the meantime. A redirect that 
does not leave the page, such as one to a file download, leaves the veil up as 
well; offer downloads through _AjaxDownloadBehavior_ instead.
+
 === Veiling a single component
 
 _LocalVeilBehavior_ veils only the component it is added to, and only during 
the AJAX requests fired by that component or by a component nested in it. The 
rest of the page stays usable:
@@ -28,7 +30,7 @@ WebMarkupContainer searchPanel = new 
WebMarkupContainer("search");
 searchPanel.add(new LocalVeilBehavior());
 ----
 
-A request fired from inside a component with a local veil does not veil the 
page, even when the page has a _PageVeilBehavior_. When local veils are nested, 
the innermost one takes the request. The behavior makes the component output 
its markup id, and it appends the veil to the component's element, so the 
component has to render an element that can hold a _div_.
+A request fired from inside a component with a local veil does not veil the 
page, even when the page has a _PageVeilBehavior_. When local veils are nested, 
the innermost one takes the request; for a behavior that listens on a container 
for events of its children, it is the veil around the element that was clicked. 
The behavior makes the component output its markup id, and it appends the veil 
to the component's element, so the component has to render an element that can 
hold a _div_. When [...]
 
 === Veiling a component during a WebSocket push
 
@@ -99,6 +101,7 @@ The look comes from the stylesheet _wicket-veil.css_, 
contributed by the behavio
 
 * _wicket-veil_: the veil itself, a _div_ covering the page (_position: 
fixed_) or the component (_position: absolute_).
 * _wicket-veil-busy_: added to the veil once the spinner delay has passed. It 
gives the veil its background, and its `::after` pseudo-element draws the 
spinner.
-* _wicket-veil-host_: added to the element of a component with a local veil 
while it is veiled. It gives the element _position: relative_, so the veil 
covers exactly that element.
+* _wicket-veil-host_: added to the element of a component with a local veil 
while it is veiled. It gives the element _isolation: isolate_, so the veil 
stays within the element's stacking context and does not paint over a sticky 
header or a modal dialog in front of it.
+* _wicket-veil-host-static_: added together with _wicket-veil-host_ when the 
element is not positioned already. It gives the element _position: relative_, 
so the veil covers exactly that element; an element that is _absolute_, _fixed_ 
or _sticky_ keeps its position.
 
-On the client side the veil is implemented by _Wicket.Veil_, which subscribes 
to the global AJAX topics '/ajax/call/beforeSend' and '/ajax/call/done' listed 
among the global AJAX call listeners later in this chapter. It works with the 
JQuery-based and with the plain JavaScript AJAX implementation alike.
+On the client side the veil is implemented by _Wicket.Veil_, which subscribes 
to the global AJAX topics '/ajax/call/beforeSend', '/ajax/call/done' and 
'/dom/node/added' listed among the global AJAX call listeners later in this 
chapter. It works with the JQuery-based and with the plain JavaScript AJAX 
implementation alike.
diff --git a/wicket-user-guide/src/main/asciidoc/ajax/ajax_6.adoc 
b/wicket-user-guide/src/main/asciidoc/ajax/ajax_6.adoc
index a940d01aeb..e7d964fb18 100644
--- a/wicket-user-guide/src/main/asciidoc/ajax/ajax_6.adoc
+++ b/wicket-user-guide/src/main/asciidoc/ajax/ajax_6.adoc
@@ -153,7 +153,7 @@ Global AJAX call events are handled with JavaScript. We can 
register a callback
 * '/ajax/call/success': called if the AJAX call has successfully returned.
 * '/ajax/call/failure': called if the AJAX call has returned with a failure.
 * '/ajax/call/complete': called when the AJAX call has completed.
-* '/ajax/call/done': called when the AJAX call is done.
+* '/ajax/call/done': called when the AJAX call is done. Its callback gets, 
after attrs, a boolean telling whether the response is redirecting the browser 
to another page, in which case the page is about to be left and anything 
blocking it can stay up.
 * '/dom/node/removing': called when a component is about to be removed via 
AJAX. This  happens when component markup is updated via AJAX (i.e. the 
component itself or one of its containers has been added to _AjaxRequestTarget_)
 * '/dom/node/added': called when a component has been added via AJAX. Just 
like '/dom/node/removing', this event is triggered when a component is added to 
_AjaxRequestTarget_.
 

Reply via email to