You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by sv...@apache.org on 2020/01/03 16:00:48 UTC

[wicket] branch master updated: WICKET-6666 modal dialog

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 9f0d903  WICKET-6666 modal dialog
9f0d903 is described below

commit 9f0d903f1aa6579e4ef2fd6e0fef99816591062f
Author: Sven Meier <sv...@apache.org>
AuthorDate: Wed Jun 19 18:09:44 2019 +0200

    WICKET-6666 modal dialog
    
    ModalWindow successor
---
 .../wicket/ajax/res/js/wicket-ajax-jquery.js       |  25 ++
 .../examples/ajax/builtin/AjaxApplication.java     |   2 +
 .../apache/wicket/examples/ajax/builtin/Index.html |   2 +-
 .../ajax/builtin/modal/ModalDialogPage.html        |  47 ++++
 .../ajax/builtin/modal/ModalDialogPage.java        | 209 +++++++++++++++
 .../ajax/builtin/modal/ModalDialogPage.properties  |  15 ++
 .../wicket/examples/ajax/builtin/modal/dialog.css  |  64 +++++
 .../ajax/markup/html/modal/ModalDialog.html        |  25 ++
 .../ajax/markup/html/modal/ModalDialog.java        | 281 +++++++++++++++++++++
 .../ajax/markup/html/modal/ModalWindow.java        |   2 +
 .../ajax/markup/html/modal/TrapFocusBehavior.java  |  58 +++++
 .../ajax/markup/html/modal/theme/DefaultTheme.java |  50 ++++
 .../ajax/markup/html/modal/theme/theme.css         |  84 ++++++
 .../ajax/markup/html/modal/trap-focus.js           | 130 ++++++++++
 .../ajax/markup/html/repeater/AjaxListPanel.html   |  22 ++
 .../ajax/markup/html/repeater/AjaxListPanel.java   | 129 ++++++++++
 .../markup/html/repeater/AjaxListPanelTest.java    |  65 +++++
 17 files changed, 1209 insertions(+), 1 deletion(-)

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 2191fb9..3647df2 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
@@ -1455,6 +1455,31 @@
 					we.publish(topic.DOM_NODE_ADDED, newElement);
 				}
 			},
+			
+			add: function (element, text) {
+				var we = Wicket.Event;
+				var topic = we.Topic;
+
+				// jQuery 1.9+ expects '<' as the very first character in text
+				var cleanedText = jQuery.trim(text);
+
+				var $newElement = jQuery(cleanedText);
+				jQuery(element).append($newElement);
+
+				var newElement = Wicket.$(element.id);
+				if (newElement) {
+					we.publish(topic.DOM_NODE_ADDED, newElement);
+				}
+			},
+
+			remove: function (element) {
+				var we = Wicket.Event;
+				var topic = we.Topic;
+
+				we.publish(topic.DOM_NODE_REMOVING, element);
+
+				jQuery(element).remove();
+			},
 
 			// Method for serializing DOM nodes to string
 			// original taken from Tacos (http://tacoscomponents.jot.com)
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java
index b43828e..01d3cd5 100644
--- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java
@@ -21,6 +21,7 @@ import org.apache.wicket.Page;
 import org.apache.wicket.ajax.AjaxNewWindowNotifyingBehavior;
 import org.apache.wicket.application.IComponentInitializationListener;
 import org.apache.wicket.examples.WicketExampleApplication;
+import org.apache.wicket.examples.ajax.builtin.modal.ModalDialogPage;
 import org.apache.wicket.examples.ajax.builtin.modal.ModalWindowPage;
 import org.apache.wicket.markup.html.WebPage;
 import org.apache.wicket.response.filter.AjaxServerAndClientTimeFilter;
@@ -63,6 +64,7 @@ public class AjaxApplication extends WicketExampleApplication
 		mountPage("lazy-loading", LazyLoadingPage.class);
 		mountPage("links", LinksPage.class);
 		mountPage("modal-window", ModalWindowPage.class);
+		mountPage("modal-dialog", ModalDialogPage.class);
 		mountPage("on-change-ajax-behavior", OnChangeAjaxBehaviorPage.class);
 		mountPage("pageables", PageablesPage.class);
 		mountPage("ratings", RatingsPage.class);
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html
index 2da2160..0d66d0a 100644
--- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html
@@ -22,7 +22,7 @@
 <br/><br/>
 <a href="FileUploadPage.html">File Upload Example</a>: demonstrates trasnparent ajax handling of a multipart form
 <br/><br/>
-<a href="modal/ModalWindowPage.html">Modal window</a>: javascript modal window example
+<a href="modal/ModalDialogPage.html">Modal dialog</a>: javascript modal dialog example
 <br/><br/>
 <a href="OnChangeAjaxBehaviorPage.html">On Change Ajax Updater Example</a>: demonstrates updating page with ajax when text field value is changed	
 <br/><br/>
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.html
new file mode 100644
index 0000000..a2bb939
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.html
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<html>
+	<body>
+		<wicket:extend xmlns:wicket="http://wicket.apache.org">
+		
+			<p wicket:id="stacked">
+				<input type="radio" wicket:id="no" /> <label wicket:for="no">nested</label>
+				<input type="radio" wicket:id="yes" /> <label wicket:for="yes">stacked dialogs</label> 
+			</p>
+			
+			<div wicket:id="start"></div>
+			
+			<div wicket:id="stackedDialogs"></div>	
+			
+			<wicket:fragment wicket:id="fragment">
+				<form wicket:id="form" class="modal-dialog-form">
+					<h1 class="modal-dialog-header">Header</h1>
+						
+					<div class="modal-dialog-body">
+						<p>
+							<input type="text" wicket:id="text" placeholder="Focus and press ENTER to open dialog" size="32" autofocus="autofocus"/>
+						</p>
+					
+						<div wicket:id="lorem"></div>	
+						<a wicket:id="ipsum">Lorem...</a>	
+			
+						<div wicket:id="nestedDialog"></div>
+					</div>
+						
+					<div class="modal-dialog-footer">
+						<a wicket:id="openDialog">Open dialog</a>
+							|
+						<a wicket:id="ajaxOpenDialog">via Ajax</a>
+							
+						<span wicket:id="closing">
+							|
+							<a wicket:id="close">Close dialog</a>
+							|
+							via Ajax by pressing ESC
+						</span>
+					</div>
+				</form>
+			</wicket:fragment>
+		
+		</wicket:extend>
+	</body>
+</html>
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.java
new file mode 100644
index 0000000..665b100
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.java
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.examples.ajax.builtin.modal;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxCallListener;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.examples.ajax.builtin.BasePage;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalDialog;
+import org.apache.wicket.extensions.ajax.markup.html.modal.theme.DefaultTheme;
+import org.apache.wicket.extensions.ajax.markup.html.repeater.AjaxListPanel;
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.MultiLineLabel;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.Radio;
+import org.apache.wicket.markup.html.form.RadioGroup;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.request.resource.CssResourceReference;
+
+/**
+ * @author Igor Vaynberg (ivaynberg)
+ */
+public class ModalDialogPage extends BasePage
+{
+
+	private AjaxListPanel stackedDialogs;
+
+	/**
+	 * Should dialogs be stacked rather than nested
+	 */
+	private boolean stacked = false;
+
+	public ModalDialogPage()
+	{
+
+		queue(new RadioGroup("stacked", new PropertyModel<>(this, "stacked"))
+			.setRenderBodyOnly(false).add(new AjaxFormChoiceComponentUpdatingBehavior()
+			{
+				@Override
+				protected void onUpdate(AjaxRequestTarget target)
+				{
+				}
+			}));
+
+		queue(new Radio<Boolean>("yes", Model.of(true)));
+		queue(new Radio<Boolean>("no", Model.of(false)));
+
+		queue(new ModalFragment("start"));
+
+		stackedDialogs = new AjaxListPanel("stackedDialogs");
+		queue(stackedDialogs);
+	}
+
+	@Override
+	public void renderHead(IHeaderResponse response)
+	{
+		super.renderHead(response);
+		
+		response.render(CssHeaderItem.forReference(new CssResourceReference(ModalDialogPage.class,
+			"dialog.css")));
+	}
+	
+	private class ModalFragment extends Fragment
+	{
+
+		private ModalDialog nestedDialog;
+
+		public ModalFragment(String id)
+		{
+			super(id, "fragment", ModalDialogPage.this);
+
+			Form<Void> form = new Form<Void>("form");
+			queue(form);
+			
+			nestedDialog = new ModalDialog("nestedDialog");
+			nestedDialog.add(new DefaultTheme());
+			nestedDialog.trapFocus();
+			nestedDialog.closeOnEscape();
+			queue(nestedDialog);
+
+			queue(new AjaxLink<Void>("ajaxOpenDialog")
+			{
+				@Override
+				public void onClick(AjaxRequestTarget target)
+				{
+					openDialog(target);
+				}
+			});
+
+			queue(new Link<Void>("openDialog")
+			{
+				@Override
+				public void onClick()
+				{
+					openDialog(null);
+				}
+			});
+
+			queue(new TextField("text").add(new AjaxEventBehavior("keydown")
+			{
+				@Override
+				protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
+				{
+					super.updateAjaxAttributes(attributes);
+					
+					attributes.getAjaxCallListeners().add(new AjaxCallListener()
+					{
+						@Override
+						public CharSequence getPrecondition(Component component)
+						{
+							return "if (Wicket.Event.keyCode(attrs.event) != 13) return false; Wicket.Event.fix(attrs.event).preventDefault(); return true;";
+						}
+					});
+				}
+
+				@Override
+				protected void onEvent(AjaxRequestTarget target)
+				{
+					openDialog(target);
+				}
+			}));
+
+			queue(new WebMarkupContainer("closing")
+			{
+				@Override
+				protected void onConfigure()
+				{
+					super.onConfigure();
+
+					setVisible(findParent(ModalDialog.class) != null);
+				}
+			});
+
+			queue(new Link<Void>("close")
+			{
+				@Override
+				public void onClick()
+				{
+					findParent(ModalDialog.class).close(null);
+				}
+			});
+			
+			final MultiLineLabel lorem = new MultiLineLabel("lorem", "");
+			lorem.setOutputMarkupId(true);
+			queue(lorem);
+			
+			queue(new AjaxLink<Void>("ipsum") {
+				@Override
+				public void onClick(AjaxRequestTarget target)
+				{
+					lorem.setDefaultModelObject(lorem.getDefaultModelObject() + "\n\n" + getString("lorem"));
+					
+					target.add(lorem);
+				}
+			});
+		}
+
+		private void openDialog(AjaxRequestTarget target)
+		{
+			ModalFragment fragment = new ModalFragment(ModalDialog.CONTENT_ID);
+			if (stacked)
+			{
+				// stack a new dialog
+				ModalDialog dialog = new ModalDialog(stackedDialogs.newChildId())
+				{
+					@Override
+					public ModalDialog close(AjaxRequestTarget target)
+					{
+						return stackedDialogs.delete(this, target);
+					}
+				};
+				dialog.add(new DefaultTheme());
+				dialog.trapFocus();
+				dialog.closeOnEscape();
+				dialog.setContent(fragment);
+				stackedDialogs.append(dialog, target).open(target);
+			}
+			else
+			{
+				// use the nested dialog
+				nestedDialog.open(fragment, target);
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.properties
new file mode 100644
index 0000000..99ebb91
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.properties
@@ -0,0 +1,15 @@
+#  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.
+lorem = Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/dialog.css b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/dialog.css
new file mode 100644
index 0000000..70764a1
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/dialog.css
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+.modal-dialog {
+	border-radius: 5px;
+}
+
+.modal-dialog .modal-dialog-content {
+	/* flex children */
+	display: flex;
+	flex-direction: column;
+}
+
+.modal-dialog-overlay.current-focus-trap .modal-dialog-content {
+	/* resize the dialog with current focus only, otherwise the resize handle shows through on Firefox */
+	resize: both;
+}
+
+.modal-dialog .modal-dialog-form {
+	/* size */
+	margin: 0;
+	padding: 0;
+	overflow: hidden;
+	
+	/* flex in parent */
+	flex: 1;
+
+	/* flex children */
+	display: flex;
+	flex-direction: column;
+}		
+			
+.modal-dialog .modal-dialog-header {
+	border-radius: 5px 5px 0px 0px;
+	background: #ffb158;
+	margin: 0;
+	padding-top: 4px;
+	text-align: center;
+}
+
+.modal-dialog .modal-dialog-body {
+	/* size */
+	flex: 1;
+	overflow-y: auto;
+	
+	padding: 20px;
+}
+
+.modal-dialog .modal-dialog-footer {
+	padding: 5px;
+}
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.html b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.html
new file mode 100644
index 0000000..7c1de89
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.html
@@ -0,0 +1,25 @@
+<?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.
+-->
+<wicket:panel xmlns:wicket="http://wicket.apache.org">
+	<div wicket:id="overlay" class="modal-dialog-overlay">
+		<div wicket:id="dialog" class="modal-dialog">
+			<div wicket:id="content" class="modal-dialog-content">
+			</div>
+		</div>
+	</div>
+</wicket:panel>
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.java
new file mode 100644
index 0000000..ce88ae4
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.java
@@ -0,0 +1,281 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.modal;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxCallListener;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.EventPropagation;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Panel;
+
+/**
+ * Presents a modal dialog to the user. See {@link #open(Component, AjaxRequestTarget)} and {@link #close(AjaxRequestTarget)} methods.
+ * 
+ * @author Igor Vaynberg (ivaynberg)
+ * @author svenmeier
+ */
+public class ModalDialog extends Panel
+{
+
+	private static final long serialVersionUID = 1L;
+
+	private static final String OVERLAY_ID = "overlay";
+
+	private static final String DIALOG_ID = "dialog";
+
+	public static final String CONTENT_ID = "content";
+
+	private WebMarkupContainer overlay;
+	
+	private WebMarkupContainer dialog;
+	
+	private boolean removeContentOnClose;
+
+	public ModalDialog(String id)
+	{
+		super(id);
+
+		setOutputMarkupId(true);
+
+		overlay = newOverlay(OVERLAY_ID);
+		overlay.setVisible(false);
+		add(overlay);
+		
+		dialog = newDialog(DIALOG_ID);
+		overlay.add(dialog);
+	}
+
+	/**
+	 * Factory method for the overlay markup around the dialog.
+	 * 
+	 * @param overlayId
+	 *            id
+	 * @return overlay
+	 */
+	protected WebMarkupContainer newOverlay(String overlayId)
+	{
+		return new WebMarkupContainer(overlayId);
+	}
+
+	/**
+	 * Factory method for the dialog markup around the content.
+	 * 
+	 * @param overlayId
+	 *            id
+	 * @return overlay
+	 */
+	protected WebMarkupContainer newDialog(String dialogId)
+	{
+		return new WebMarkupContainer(dialogId);
+	}
+
+	/**
+	 * Set a content.
+	 * 
+	 * @param content
+	 * 
+	 * @see #open(AjaxRequestTarget)
+	 */
+	public void setContent(Component content)
+	{
+		if (!content.getId().equals(CONTENT_ID))
+		{
+			throw new IllegalArgumentException(
+				"Content must have wicket id set to ModalDialog.CONTENT_ID");
+		}
+
+		dialog.addOrReplace(content);
+
+		removeContentOnClose = false;
+	}
+
+	/**
+	 * Open the dialog with a content.
+	 * <p>
+	 * The content will be removed on close of the dialog.
+	 * 
+	 * @param content
+	 *            the content
+	 * @param target
+	 *            an optional Ajax target
+	 * @return this
+	 * 
+	 * @see #close(AjaxRequestTarget)
+	 */
+	public ModalDialog open(Component content, AjaxRequestTarget target)
+	{
+		setContent(content);
+		removeContentOnClose = true;
+		
+		overlay.setVisible(true);
+
+		if (target != null)
+		{
+			target.add(this);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Open the dialog.
+	 * 
+	 * @param target
+	 *            an optional Ajax target
+	 * @return this
+	 * 
+	 * @see #setContent(Component)
+	 */
+	public ModalDialog open(AjaxRequestTarget target)
+	{
+		if (overlay.size() == 0) {
+			throw new WicketRuntimeException("no content set");
+		}
+		
+		overlay.setVisible(true);
+
+		if (target != null)
+		{
+			target.add(this);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Is this dialog open.
+	 * 
+	 * @return <code>true</code> if open
+	 */
+	public boolean isOpen()
+	{
+		return overlay.isVisible();
+	}
+
+	/**
+	 * Close this dialog.
+	 * <p>
+	 * If opened via {@link #open(Component, AjaxRequestTarget)}, the content is removed from the component tree
+	 * 
+	 * @param target
+	 *            an optional Ajax target
+	 * @return this
+	 * 
+	 * @see #open(Component, AjaxRequestTarget)
+	 */
+	public ModalDialog close(AjaxRequestTarget target)
+	{
+		overlay.setVisible(false);
+		if (removeContentOnClose) {
+			dialog.removeAll();
+		}
+
+		if (target != null)
+		{
+			target.add(this);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Close this dialog on press of escape key.
+	 * 
+	 * @return this
+	 */
+	public ModalDialog closeOnEscape()
+	{
+		overlay.add(new CloseBehavior("keydown")
+		{
+			protected CharSequence getPrecondition()
+			{
+				return "return Wicket.Event.keyCode(attrs.event) == 27";
+			}
+		});
+		return this;
+	}
+
+	/**
+	 * Close this dialog on click outside.
+	 * 
+	 * @return this
+	 */
+	public ModalDialog closeOnClick()
+	{
+		overlay.add(new CloseBehavior("click") {
+			protected CharSequence getPrecondition()
+			{
+				return String.format("return attrs.event.target.id == '%s';", overlay.getMarkupId());
+			}
+		});
+		return this;
+	}
+
+	/**
+	 * Convenience method to trap focus inside the overlay.
+	 * 
+	 * @see {@link TrapFocusBehavior}
+	 * 
+	 * @return this
+	 */
+	public ModalDialog trapFocus()
+	{
+		overlay.add(new TrapFocusBehavior());
+
+		return this;
+	}
+	
+	private abstract class CloseBehavior extends AjaxEventBehavior
+	{
+		private CloseBehavior(String event)
+		{
+			super(event);
+		}
+
+		@Override
+		protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
+		{
+			super.updateAjaxAttributes(attributes);
+
+			// has to stop immediately to prevent an enclosing dialog to close too 
+			attributes.setEventPropagation(EventPropagation.STOP_IMMEDIATE);
+
+			attributes.getAjaxCallListeners().add(new AjaxCallListener()
+			{
+				@Override
+				public CharSequence getPrecondition(Component component)
+				{
+					return CloseBehavior.this.getPrecondition();
+				}
+			});
+		}
+
+		protected CharSequence getPrecondition() {
+			return "";
+		}
+
+		@Override
+		protected void onEvent(AjaxRequestTarget target)
+		{
+			close(target);
+		}
+	}
+}
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalWindow.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalWindow.java
index 7cc3f36..5a8318c 100644
--- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalWindow.java
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalWindow.java
@@ -121,6 +121,8 @@ import com.github.openjson.JSONObject;
  * before the window get closed.
  * 
  * @author Matej Knopp
+ * 
+ * @deprecated use {@link ModalDialog} instead
  */
 public class ModalWindow extends Panel
 {
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/TrapFocusBehavior.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/TrapFocusBehavior.java
new file mode 100644
index 0000000..0721119
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/TrapFocusBehavior.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.modal;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.core.util.string.CssUtils;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+import org.apache.wicket.markup.head.PriorityHeaderItem;
+import org.apache.wicket.request.resource.JavaScriptResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+
+/**
+ * Trap focus inside a component's markup.
+ * 
+ * @author svenmeier
+ */
+public class TrapFocusBehavior extends Behavior
+{
+
+	/**
+	 * Resource key for a CSS class to be applied to the current active focus-trap. 
+	 */
+	public static final String CSS_CURRENT_KEY = CssUtils.key(TrapFocusBehavior.class, "current");
+
+	private static final long serialVersionUID = 1L;
+	
+	private static final ResourceReference JS = new JavaScriptResourceReference(
+		TrapFocusBehavior.class, "trap-focus.js");
+
+	@Override
+	public void renderHead(Component component, IHeaderResponse response)
+	{
+		response.render(JavaScriptHeaderItem.forReference(JS));
+		
+		String styleClass = component.getString(CSS_CURRENT_KEY, null, "current-focus-trap");
+		
+		CharSequence script = String.format("Wicket.trapFocus('%s', '%s');", component.getMarkupId(), styleClass);
+		
+		response.render(new PriorityHeaderItem(OnDomReadyHeaderItem.forScript(script)));
+	}
+}
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/DefaultTheme.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/DefaultTheme.java
new file mode 100644
index 0000000..f408d07
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/DefaultTheme.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.modal.theme;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalDialog;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.request.resource.CssResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+
+/**
+ * Default theme for {@link ModalDialog}.
+ * 
+ * @author svenmeier
+ */
+public class DefaultTheme extends Behavior
+{
+	private static final long serialVersionUID = 1L;
+
+	private static final ResourceReference CSS = new CssResourceReference(DefaultTheme.class, "theme.css");
+
+	@Override
+	public void onComponentTag(Component component, ComponentTag tag)
+	{
+		tag.append("class", "dialog-theme-default", " ");
+	}
+
+	@Override
+	public void renderHead(Component component, IHeaderResponse response)
+	{
+		response.render(CssHeaderItem.forReference(CSS));
+	}
+}
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/theme.css b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/theme.css
new file mode 100644
index 0000000..7b75e10
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/theme.css
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+.dialog-theme-default .modal-dialog-overlay {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	margin: 0;
+	padding: 0;
+	z-index: 1000;
+	
+	background: rgba(0, 0, 0, 0.2);
+
+	/* center .modal-dialog */
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+}
+
+.dialog-theme-default .modal-dialog {
+	position: absolute;
+	top: 10%;
+	
+	background: white;
+	box-shadow: 0 0 60px 10px rgba(0, 0, 0, 0.7);
+}
+
+.modal-dialog .modal-dialog-content {
+	min-width: 20vw;
+	max-width: 80vw;
+	min-height: 20vh;
+	max-height: 80vh;
+	overflow: auto;
+}
+
+/* shift nested dialogs */
+
+.dialog-theme-default .modal-dialog .modal-dialog {
+	margin-top: 16px;
+	margin-left: 16px;
+}
+
+.dialog-theme-default .modal-dialog .modal-dialog .modal-dialog {
+	margin-top: 32px;
+	margin-left: 32px;
+}
+
+.dialog-theme-default .modal-dialog .modal-dialog .modal-dialog .modal-dialog {
+	margin-top: 48px;
+	margin-left: 48px;
+}
+
+/* shift stacked dialogs */
+
+.dialog-theme-default ~ .dialog-theme-default .modal-dialog {
+	margin-top: 16px;
+	margin-left: 16px;
+}
+
+.dialog-theme-default ~ .dialog-theme-default ~ .dialog-theme-default .modal-dialog {
+	margin-top: 32px;
+	margin-left: 32px;
+}
+
+.dialog-theme-default ~ .dialog-theme-default ~ .dialog-theme-default ~ .dialog-theme-default .modal-dialog {
+	margin-top: 48px;
+	margin-left: 48px;
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..63bf36f
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+/*
+ * Used by TrapFocusBehavior to trap focus inside a component's markup.
+ *
+ * @author Igor Vaynberg
+ * @author svenmeier
+ */
+;
+(function($, window, document, undefined) {
+	'use strict';
+
+	if (window.Wicket && window.Wicket.trapFocus) {
+		return;
+	}
+	
+	/** find all elements inside container that can receive focus */
+	function findFocusable(container) {
+		var focusables = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]';
+		return container.find(focusables).filter(":visible, *:not([tabindex=-1])");
+	}
+
+	// special handler notified by jQuery on removal of a 'trapfocusremove' handler - this
+	// happens whenever an element with a focus trap is removed from the DOM, see below
+	$.event.special.trapfocusremove = {
+		remove: function(handleObj) {
+			// forward removal notification, this allows the focus trap to be cleaned up  
+			handleObj.handler();
+		}
+	};
+
+	// one global active 'focusin' handler for all traps  
+	var focusin = $.noop;
+
+	// setup a focus trap for an element
+	window.Wicket.trapFocus = function(element, styleClass) {
+		
+		var $element = $('#' + element);
+		
+		// keep old active element
+		var oldActive = document.activeElement;
+		Wicket.Log.debug("trap-focus: focus was on element", oldActive);
+
+		// allow focus on element itself
+		$element.attr('tabindex', 0);
+		
+		// handles focus navigation via tab key
+		$element.on("keydown", function(e) {
+			if (Wicket.Event.keyCode(e) === 9) { // tab
+				var $focusable = findFocusable($element);
+				if ($focusable.length > 0) {
+					var firstFocusable = $focusable.get(0);
+					var lastFocusable  = $focusable.get($focusable.length - 1);
+
+					if (e.shiftKey) {
+						if (e.target === firstFocusable || $element.is(e.target)) {
+							e.preventDefault();
+							lastFocusable.focus();
+						}
+					} else {
+						if (e.target === lastFocusable || $element.is(e.target)) {
+							e.preventDefault();
+							firstFocusable.focus();
+						}
+					}
+				}
+			}
+		});
+		
+		// mark current trap
+		var oldTrap = $('.' + styleClass).removeClass(styleClass);
+		$element.addClass(styleClass);
+
+		// turn off previous 'focusin' handler
+		var previousfocusin = focusin;
+		$(document).off("focusin", focusin);
+
+		// ... pull in focus
+		findFocusable($element).first().focus();
+		
+		// ... and install new handler
+		focusin = function() {
+			if (!$.contains($element[0], document.activeElement) && $element[0] !== document.activeElement) {
+				// focus is outside of element, so pull in focus
+				findFocusable($element).first().focus();
+			}
+		};
+		$(document).on("focusin", focusin);
+		
+		// listen for removal
+		$element.on("trapfocusremove", function() {
+			// turn off 'focusin' handler
+			$(document).off("focusin", focusin);
+			
+			// ... restore old focus
+			if (oldActive) {
+				try {
+					oldActive.focus();
+					Wicket.Log.debug("trap-focus: restored focus to element ", oldActive);
+				} catch (error) {
+					Wicket.Log.error("trap-focus: error restoring focus. Attempted to set focus to element, but got an exception", oldActive, error);
+				}
+			}
+			
+			// ... re-install previous 'focusin' handler
+			focusin = previousfocusin;
+			$(document).on("focusin", focusin);
+			
+			// ... and restore trap mark
+			oldTrap.addClass(styleClass);
+			$element.removeClass(styleClass);
+		});
+	};
+
+}(jQuery, window, document, undefined));
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.html b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.html
new file mode 100644
index 0000000..8f9aa2e
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.html
@@ -0,0 +1,22 @@
+<?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.
+-->
+<wicket:panel xmlns:wicket="http://wicket.apache.org">
+	<div wicket:id="container">
+		<div wicket:id="repeater"></div>
+	</div>
+</wicket:panel>
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.java
new file mode 100644
index 0000000..b5ad0ce
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.repeater;
+
+import java.util.EmptyStackException;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.core.util.string.JavaScriptUtils;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.IMarkupFragment;
+import org.apache.wicket.markup.MarkupStream;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.parser.XmlTag.TagType;
+import org.apache.wicket.markup.repeater.RepeatingView;
+
+/**
+ * An panel for an <it>Ajax-ified</it> list of components.
+ * <p>
+ * Allows to dynamically append and delete components without an update of a whole container.
+ * 
+ * @see #append(Component, AjaxRequestTarget)
+ * @see #delete(Component, AjaxRequestTarget)
+ * 
+ * @author svenmeier
+ */
+public class AjaxListPanel extends Panel
+{
+
+	private static final long serialVersionUID = 1L;
+	
+	private WebMarkupContainer container;
+
+	private RepeatingView repeater;
+
+	public AjaxListPanel(String id)
+	{
+		super(id);
+
+		this.container = new WebMarkupContainer("container");
+		this.container.setOutputMarkupId(true);
+		add(this.container);
+
+		this.repeater = new RepeatingView("repeater");
+		this.container.add(this.repeater);
+	}
+
+	/**
+	 * Get an id for a new child to be appended.
+	 * 
+	 * @return id
+	 * 
+	 * @see #append(Component, AjaxRequestTarget)
+	 */
+	public String newChildId() {
+		return repeater.newChildId();
+	}
+	
+	/**
+	 * Append a component.
+	 * 
+	 * @param component
+	 *            the component
+	 * @param target
+	 *            optional target
+	 * @return the component
+	 * 
+	 * @param T component type
+	 */
+	public <T extends Component> T append(T component, AjaxRequestTarget target)
+	{
+		this.repeater.add(component);
+		
+		if (target != null)
+		{
+			IMarkupFragment markup = repeater.getMarkup();
+			
+			// append markup to be updated
+			MarkupStream stream = new MarkupStream(markup);
+			ComponentTag tag = stream.getTag().mutable();
+			tag.getXmlTag().setType(TagType.OPEN_CLOSE);
+			tag.getXmlTag().put("id", component.getMarkupId());
+
+			target.prependJavaScript(String.format("Wicket.DOM.add(Wicket.DOM.get('%s'), '%s');",
+				container.getMarkupId(), JavaScriptUtils.escapeQuotes(tag.toString())));
+			
+			// ... then update the appended component 
+			target.add(component);
+		}
+
+		return component;
+	}
+	
+	/**
+	 * Delete a component.
+	 * 
+	 * @param target
+	 *            optional target
+	 * @return the component
+	 * @throws EmptyStackException if empty
+	 * 
+	 * @param T component type
+	 */
+	public <T extends Component> T delete(T component, AjaxRequestTarget target) {
+
+		this.repeater.remove(component);
+		if (target != null)
+		{
+			target.appendJavaScript(String.format("Wicket.DOM.remove(Wicket.DOM.get('%s'));", component.getMarkupId()));
+		}
+		
+		return (T)component;
+	}
+}
\ No newline at end of file
diff --git a/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanelTest.java b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanelTest.java
new file mode 100644
index 0000000..9b4022e
--- /dev/null
+++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanelTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.repeater;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for {@link AjaxListPanel}.
+ * 
+ * @author svenmeier
+ */
+class AjaxListPanelTest extends WicketTestCase
+{
+
+	@Test
+	void test()
+	{
+		final AjaxListPanel list = new AjaxListPanel("list");
+
+		AjaxEventBehavior behavior = new AjaxEventBehavior("click") {
+			Label label;
+			
+			@Override
+			protected void onEvent(AjaxRequestTarget target)
+			{
+				if (label == null) {
+					label = new Label(list.newChildId());
+					list.append(label, target);
+				} else {
+					list.delete(label, target);
+					label = null;
+				}
+			}
+		};
+		list.add(behavior);
+		
+		tester.startComponentInPage(list);
+		
+		tester.executeBehavior(behavior);
+		assertTrue(tester.getLastResponseAsString().contains("Wicket.DOM.add(Wicket.DOM.get('container2'), '<div wicket:id=\\\"repeater\\\" id=\\\"id13\\\"/>');"));
+
+		tester.executeBehavior(behavior);
+		assertTrue(tester.getLastResponseAsString().contains("Wicket.DOM.remove(Wicket.DOM.get('id13'));"));
+	}
+}