This is an automated email from the ASF dual-hosted git repository. reiern70 pushed a commit to branch wicket-10.x in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 407486d058344441683d74e7f4ec763d3acec243 Author: reiern70 <[email protected]> AuthorDate: Sat Sep 26 09:26:42 2026 -0500 Fix TrapFocusBehavior throwing TypeError when Tab wraps focus Wicket.trapFocus's keydown handler resolves the first and last focusable elements with $focusable.get(0) and $focusable.get($focusable.length - 1), which are plain DOM elements, then called .trigger('focus') on them. Tabbing past the last focusable element (or Shift+Tabbing past the first) inside a focus-trapped container - e.g. a ModalDialog - threw a TypeError instead of wrapping focus around, so keyboard users got stuck instead of cycling back to the other end of the trap. The other two calls in the same file already went through findFocusable(...).first(), which returns a jQuery object, so they were unaffected. This is the same class of bug as the Palette "move up" issue: both were introduced by the jQuery 4.0.0 migration, which mechanically rewrote box.focus() as box.trigger('focus') without checking whether the receiver was a jQuery object. master does not have this bug; it later dropped jQuery from this file and uses .focus() directly. Adds a QUnit regression test asserting that Tab/Shift+Tab at the trap's boundaries move focus to the other end without throwing. GitHub issue #1620 --- testing/wicket-js-tests/Gruntfile.js | 7 +- .../ajax/markup/html/modal/trap-focus.js | 4 +- wicket-extensions/src/test/js/trap-focus.html | 50 +++++++++++++++ wicket-extensions/src/test/js/trapfocus-test.js | 75 ++++++++++++++++++++++ 4 files changed, 132 insertions(+), 4 deletions(-) diff --git a/testing/wicket-js-tests/Gruntfile.js b/testing/wicket-js-tests/Gruntfile.js index 62ea0c8fe0..c89f34e316 100644 --- a/testing/wicket-js-tests/Gruntfile.js +++ b/testing/wicket-js-tests/Gruntfile.js @@ -46,7 +46,8 @@ module.exports = function(grunt) { "../../wicket-core/src/test/js/timer.js" ], extensionsTestsJs = [ - "../../wicket-extensions/src/test/js/palette-test.js" + "../../wicket-extensions/src/test/js/palette-test.js", + "../../wicket-extensions/src/test/js/trapfocus-test.js" ], gymTestsJs = [ "../../wicket-examples/src/main/webapp/js-test/tests/ajax/form.js", @@ -113,7 +114,9 @@ module.exports = function(grunt) { 'http://localhost:38887/test/js/all.html?4.0.0', 'http://localhost:38887/test/js/all.html?3.7.1', 'http://localhost:38888/wicket-extensions/src/test/js/palette.html?4.0.0', - 'http://localhost:38888/wicket-extensions/src/test/js/palette.html?3.7.1' + 'http://localhost:38888/wicket-extensions/src/test/js/palette.html?3.7.1', + 'http://localhost:38888/wicket-extensions/src/test/js/trap-focus.html?4.0.0', + 'http://localhost:38888/wicket-extensions/src/test/js/trap-focus.html?3.7.1' ], puppeteer: { headless: true, diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js index d8b49822ac..ffa8b137fa 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js @@ -70,12 +70,12 @@ if (e.shiftKey) { if (e.target === firstFocusable || $element.is(e.target)) { e.preventDefault(); - lastFocusable.trigger('focus'); + lastFocusable.focus(); } } else { if (e.target === lastFocusable || $element.is(e.target)) { e.preventDefault(); - firstFocusable.trigger('focus'); + firstFocusable.focus(); } } } diff --git a/wicket-extensions/src/test/js/trap-focus.html b/wicket-extensions/src/test/js/trap-focus.html new file mode 100644 index 0000000000..dba645e596 --- /dev/null +++ b/wicket-extensions/src/test/js/trap-focus.html @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<html> + +<head> + <title id="titleId">Wicket.trapFocus 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" /> +</head> + +<body> + <div id="qunit"></div> + + <div id="qunit-fixture"> + <div id="trapFocusContainer"> + <a id="trapFocusFirst" href="#first">first</a> + <a id="trapFocusLast" href="#last">last</a> + </div> + </div> + + <script> + // version lies between question mark and first ampersand (or end) + var version = location.search.match(/\?(.*?)(&|$)/)[1]; + + document.write("<scr"+"ipt src='/wicket-core/src/main/java/org/apache/wicket/resource/jquery/jquery-"+version+".js'></scr"+"ipt>"); + </script> + <script src="/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js"></script> + <script type="text/javascript" src="/wicket-core/src/test/js/qunit/qunit.js"></script> + + <!-- the module under test --> + <script type="text/javascript" src="/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js"></script> + + <script type="text/javascript" src="trapfocus-test.js"></script> +</body> +</html> diff --git a/wicket-extensions/src/test/js/trapfocus-test.js b/wicket-extensions/src/test/js/trapfocus-test.js new file mode 100644 index 0000000000..ffecf2e0ff --- /dev/null +++ b/wicket-extensions/src/test/js/trapfocus-test.js @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*global QUnit: true */ + +jQuery(document).ready(function() { + "use strict"; + + const { module, test } = QUnit; + + const TAB_KEYCODE = 9; + + function tab(target, shiftKey) { + jQuery(target).trigger(jQuery.Event('keydown', { which: TAB_KEYCODE, keyCode: TAB_KEYCODE, shiftKey: !!shiftKey })); + } + + module("Wicket.trapFocus", { + afterEach: function() { + // detach the trap's global 'focusin' handler and reset state between tests + jQuery(document.getElementById('trapFocusContainer')).trigger('trapfocusremove'); + } + }); + + test("Tab on the last focusable element wraps focus to the first, without throwing", assert => { + window.Wicket.trapFocus('trapFocusContainer', 'wicket-trap-focus-test'); + + var first = document.getElementById('trapFocusFirst'); + var last = document.getElementById('trapFocusLast'); + last.focus(); + + var threw = false; + try { + tab(last, false); + } catch (e) { + threw = true; + } + + assert.notOk(threw, "tabbing past the last focusable element threw an exception"); + assert.equal(document.activeElement, first, + "focus did not wrap around to the first focusable element"); + }); + + test("Shift+Tab on the first focusable element wraps focus to the last, without throwing", assert => { + window.Wicket.trapFocus('trapFocusContainer', 'wicket-trap-focus-test'); + + var first = document.getElementById('trapFocusFirst'); + var last = document.getElementById('trapFocusLast'); + first.focus(); + + var threw = false; + try { + tab(first, true); + } catch (e) { + threw = true; + } + + assert.notOk(threw, "shift-tabbing past the first focusable element threw an exception"); + assert.equal(document.activeElement, last, + "focus did not wrap around to the last focusable element"); + }); +});
