Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: extract overlay auto add controller #7028

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,15 @@
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.Shortcuts;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.SlotUtils;
import com.vaadin.flow.component.shared.internal.OverlayAutoAddController;
import com.vaadin.flow.component.shared.internal.OverlayClassListProxy;
import com.vaadin.flow.dom.ClassList;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.Style;
import com.vaadin.flow.internal.StateTree;
import com.vaadin.flow.router.NavigationTrigger;
import com.vaadin.flow.shared.Registration;

/**
Expand Down Expand Up @@ -105,8 +102,6 @@ public CancelEvent(ConfirmDialog source, boolean fromClient) {
private String height;
private String width;

private Registration afterProgrammaticNavigationListenerRegistration;

/**
* Sets the width of the component content area.
* <p>
Expand Down Expand Up @@ -217,21 +212,16 @@ public Optional<String> getAriaDescribedBy() {
getElement().getProperty("accessibleDescriptionRef"));
}

private boolean autoAddedToTheUi;

/**
* Creates an empty dialog with a Confirm button
*/
public ConfirmDialog() {
getElement().addEventListener("opened-changed", event -> {
if (!isOpened()) {
setModality(false);
}
if (autoAddedToTheUi && !isOpened()) {
getElement().removeFromParent();
autoAddedToTheUi = false;
}
});
// Initialize auto-add behavior
new OverlayAutoAddController<>(this, () -> true);

addConfirmListener(event -> close());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are those listener added intentionally? Sounds like a breaking change that every confirm click automatically closes the dialog.

Example:

  • user clicks confirm
  • Application does some expensive task and failed
  • Confirm dialog is closed even tho the task failed and user should try to click confirm again to execute the task again

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently all of those close the dialog already: https://github.com/vaadin/web-components/blob/e76c4576c2d1875e16681677bbf829637bf48e4f/packages/confirm-dialog/src/vaadin-confirm-dialog-mixin.js#L435-L451

But I was thinking about using a different approach as well, it seems better to stick with the opened-changed event than make assumptions what individual actions would do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thanks! Didn't know the web component does that already (I'm using my own confirm dialog based on the normal dialog API)

Interesting.. another thing to keep in mind when using the official confirm dialog.

addRejectListener(event -> close());
addCancelListener(event -> close());
}

/**
Expand Down Expand Up @@ -648,7 +638,6 @@ public void close() {
setOpened(false);
}

@Synchronize(property = "opened", value = "opened-changed")
public boolean isOpened() {
return getElement().getProperty("opened", false);
}
Expand All @@ -664,9 +653,6 @@ public boolean isOpened() {
* close it
*/
public void setOpened(boolean opened) {
if (opened) {
ensureAttached();
}
setModality(opened);
getElement().setProperty("opened", opened);
}
Expand Down Expand Up @@ -873,6 +859,9 @@ public Component getComponentAt(int index) {
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);

updateWidth();
updateHeight();

// Same as https://github.com/vaadin/flow-components/pull/725
Shortcuts.setShortcutListenOnElement("this._overlayElement", this);
}
Expand All @@ -882,45 +871,4 @@ private void setModality(boolean modal) {
getUI().ifPresent(ui -> ui.setChildComponentModal(this, modal));
}
}

private UI getCurrentUI() {
UI ui = UI.getCurrent();
if (ui == null) {
throw new IllegalStateException("UI instance is not available. "
+ "It means that you are calling this method "
+ "out of a normal workflow where it's always implicitly set. "
+ "That may happen if you call the method from the custom thread without "
+ "'UI::access' or from tests without proper initialization.");
}
return ui;
}

private void ensureAttached() {
UI ui = getCurrentUI();
StateTree.ExecutionRegistration addToUiRegistration = ui
.beforeClientResponse(ui, context -> {
if (getElement().getNode().getParent() == null) {
ui.addToModalComponent(this);
autoAddedToTheUi = true;
updateWidth();
updateHeight();
ui.setChildComponentModal(this, true);
}
if (afterProgrammaticNavigationListenerRegistration != null) {
afterProgrammaticNavigationListenerRegistration
.remove();
}
});
if (ui.getSession() != null) {
afterProgrammaticNavigationListenerRegistration = ui
.addAfterNavigationListener(event -> {
if (event.getLocationChangeEvent()
.getTrigger() == NavigationTrigger.PROGRAMMATIC) {
addToUiRegistration.remove();
afterProgrammaticNavigationListenerRegistration
.remove();
}
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.HasThemeVariant;
import com.vaadin.flow.component.shared.internal.OverlayAutoAddController;
import com.vaadin.flow.component.shared.internal.OverlayClassListProxy;
import com.vaadin.flow.dom.ClassList;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementConstants;
import com.vaadin.flow.dom.ElementDetachEvent;
import com.vaadin.flow.dom.ElementDetachListener;
import com.vaadin.flow.dom.Style;
import com.vaadin.flow.internal.StateTree;
import com.vaadin.flow.router.NavigationTrigger;
import com.vaadin.flow.shared.Registration;

/**
Expand Down Expand Up @@ -87,7 +86,6 @@ public class Dialog extends Component implements HasComponents, HasSize,

private static final String OVERLAY_LOCATOR_JS = "this.$.overlay";

private boolean autoAddedToTheUi;
private int configuredCloseActionListeners;
private String minWidth;
private String maxWidth;
Expand All @@ -96,8 +94,6 @@ public class Dialog extends Component implements HasComponents, HasSize,
private DialogHeader dialogHeader;
private DialogFooter dialogFooter;

private Registration afterProgrammaticNavigationListenerRegistration;

/**
* Creates an empty dialog.
*/
Expand Down Expand Up @@ -132,6 +128,9 @@ public Dialog() {
});

setOverlayRole("dialog");

// Initialize auto-add behavior
new OverlayAutoAddController<>(this, this::isModal);
}

/**
Expand Down Expand Up @@ -884,46 +883,6 @@ public void setVisible(boolean visible) {
ui -> ui.setChildComponentModal(this, visible && isModal()));
}

private UI getCurrentUI() {
UI ui = UI.getCurrent();
if (ui == null) {
throw new IllegalStateException("UI instance is not available. "
+ "It means that you are calling this method "
+ "out of a normal workflow where it's always implicitly set. "
+ "That may happen if you call the method from the custom thread without "
+ "'UI::access' or from tests without proper initialization.");
}
return ui;
}

private void ensureAttached() {
UI ui = getCurrentUI();
StateTree.ExecutionRegistration addToUiRegistration = ui
.beforeClientResponse(ui, context -> {
if (getElement().getNode().getParent() == null
&& isOpened()) {
ui.addToModalComponent(this);
ui.setChildComponentModal(this, isModal());
autoAddedToTheUi = true;
}
if (afterProgrammaticNavigationListenerRegistration != null) {
afterProgrammaticNavigationListenerRegistration
.remove();
}
});
if (ui.getSession() != null) {
afterProgrammaticNavigationListenerRegistration = ui
.addAfterNavigationListener(event -> {
if (event.getLocationChangeEvent()
.getTrigger() == NavigationTrigger.PROGRAMMATIC) {
addToUiRegistration.remove();
afterProgrammaticNavigationListenerRegistration
.remove();
}
});
}
}

/**
* Registers event listeners on the dialog's overlay that prevent it from
* closing itself on outside click and escape press. Instead, the event
Expand Down Expand Up @@ -982,12 +941,6 @@ public void setOpened(boolean opened) {
}

private void doSetOpened(boolean opened, boolean fromClient) {
if (opened) {
ensureAttached();
} else if (autoAddedToTheUi) {
getElement().removeFromParent();
autoAddedToTheUi = false;
}
setModality(opened && isModal());
getElement().setProperty("opened", opened);
fireEvent(new OpenedChangeEvent(this, fromClient));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* Licensed 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 com.vaadin.flow.component.shared.internal;

import java.io.Serializable;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.function.SerializableSupplier;
import com.vaadin.flow.internal.StateTree;
import com.vaadin.flow.router.NavigationTrigger;
import com.vaadin.flow.shared.Registration;

/**
* An internal controller for automatically adding a component to the UI when
* it's opened. Not intended to be used publicly.
*
* @param <C>
* Type of the component that uses this controller.
*/
public class OverlayAutoAddController<C extends Component>
implements Serializable {
private final C component;
private final SerializableSupplier<Boolean> isModalSupplier;

private boolean autoAdded;
private Registration afterProgrammaticNavigationListenerRegistration;

public OverlayAutoAddController(C component) {
this(component, () -> false);
}

public OverlayAutoAddController(C component,
SerializableSupplier<Boolean> isModalSupplier) {
this.component = component;
this.isModalSupplier = isModalSupplier;

component.getElement().addPropertyChangeListener("opened", event -> {
if (isOpened()) {
handleOpen();
} else {
handleClose();
}
});
}

private void handleOpen() {
UI ui = getUI();
StateTree.ExecutionRegistration addToUiRegistration = ui
.beforeClientResponse(ui, context -> {
if (isOpened() && !isAttached()) {
ui.addToModalComponent(component);
ui.setChildComponentModal(component,
isModalSupplier.get());
autoAdded = true;
}
if (afterProgrammaticNavigationListenerRegistration != null) {
afterProgrammaticNavigationListenerRegistration
.remove();
}
});
if (ui.getSession() != null) {
afterProgrammaticNavigationListenerRegistration = ui
.addAfterNavigationListener(event -> {
if (event.getLocationChangeEvent()
.getTrigger() == NavigationTrigger.PROGRAMMATIC) {
addToUiRegistration.remove();
afterProgrammaticNavigationListenerRegistration
.remove();
}
});
}
}

private void handleClose() {
if (!isOpened() && autoAdded) {
autoAdded = false;
component.getElement().removeFromParent();
}
}

private UI getUI() {
UI ui = UI.getCurrent();
if (ui == null) {
throw new IllegalStateException("UI instance is not available. "
+ "It means that you are calling this method "
+ "out of a normal workflow where it's always implicitly set. "
+ "That may happen if you call the method from the custom thread without "
+ "'UI::access' or from tests without proper initialization.");
}
return ui;
}

private boolean isOpened() {
return component.getElement().getProperty("opened", false);
}

private boolean isAttached() {
return component.getElement().getNode().getParent() != null;
}
}
Loading