fix: make dashboard widgets closable (#14)
CI / build-and-test (pull_request) Successful in 1m27s
CI / build-and-test (pull_request) Successful in 1m27s
Every GridStackItem now renders a close button next to its drag grip, mirroring the grip's hover affordance. Closing detaches the item server-side; grid-stack.ts already unregisters widgets via its MutationObserver, so the layout is persisted without an extra protocol. Closable is on by default and can be turned off per item with setClosable(false); GridStackItem.CloseEvent lets views react. The GridStackView "remove last" toolbar button is dropped — per-widget close replaces it, so the view no longer tracks a widget stack. Its counter now only grows, keeping a closed widget's id from being handed to a new widget (which would inherit the saved position). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ue9ZtWUBQF4SuSHpzZ3zwq
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package com.example.components;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.ComponentEvent;
|
||||
import com.vaadin.flow.component.ComponentEventListener;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.shared.Registration;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -12,7 +15,10 @@ import java.util.UUID;
|
||||
* <p>
|
||||
* Dragging starts from the grip handle only, never from the item body, so the
|
||||
* components inside an item stay interactive (see
|
||||
* {@link GridStackLayout#setDragHandle(String)}).
|
||||
* {@link GridStackLayout#setDragHandle(String)}). A close button sits next to
|
||||
* the grip (see {@link #setClosable(boolean)}); removing the item server-side
|
||||
* is enough to unregister the widget client-side, since grid-stack.ts observes
|
||||
* its own childList.
|
||||
*/
|
||||
public class GridStackItem extends Div {
|
||||
|
||||
@@ -20,8 +26,14 @@ public class GridStackItem extends Div {
|
||||
* {@link GridStackLayout}'s {@code handle} option. */
|
||||
public static final String DRAG_HANDLE_CLASS = "dialect-drag-handle";
|
||||
|
||||
/** Marker class for the close button; {@code styles.css} keys both its own
|
||||
* styling and the grip's offset off it. */
|
||||
public static final String CLOSE_BUTTON_CLASS = "dialect-close-button";
|
||||
|
||||
private final Div content = new Div();
|
||||
private final Div dragHandle = new Div();
|
||||
private final Div closeButton = new Div();
|
||||
private boolean closable = true;
|
||||
|
||||
public GridStackItem(Component... content) {
|
||||
this(UUID.randomUUID().toString(), 0, 0, 4, 3, content);
|
||||
@@ -51,6 +63,72 @@ public class GridStackItem extends Div {
|
||||
dragHandle.getElement().setAttribute("title", label);
|
||||
dragHandle.getElement().setAttribute("aria-label", label);
|
||||
getElement().appendChild(dragHandle.getElement());
|
||||
|
||||
// Sibling of the content wrapper for the same reason as the grip, and
|
||||
// outside the drag-handle selector so clicking it never starts a drag.
|
||||
closeButton.addClassName(CLOSE_BUTTON_CLASS);
|
||||
closeButton.add(Fa.CLOSE.create());
|
||||
String closeLabel = getTranslation("gridstack.close");
|
||||
closeButton.getElement().setAttribute("title", closeLabel);
|
||||
closeButton.getElement().setAttribute("aria-label", closeLabel);
|
||||
closeButton.getElement().setAttribute("role", "button");
|
||||
closeButton.getElement().setAttribute("tabindex", "0");
|
||||
closeButton.getElement().addEventListener("click", e -> close(true));
|
||||
closeButton.getElement().addEventListener("keydown", e -> close(true))
|
||||
.setFilter("event.key === 'Enter' || event.key === ' '");
|
||||
getElement().appendChild(closeButton.getElement());
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches this item from its layout and notifies
|
||||
* {@link #addCloseListener(ComponentEventListener) close listeners}. Called
|
||||
* by the close button, and usable server-side to close an item
|
||||
* programmatically.
|
||||
*/
|
||||
public void close() {
|
||||
close(false);
|
||||
}
|
||||
|
||||
private void close(boolean fromClient) {
|
||||
// Detach first so listeners observe the layout they are about to see —
|
||||
// getLayout() on the grid no longer counts this item.
|
||||
getElement().removeFromParent();
|
||||
fireEvent(new CloseEvent(this, fromClient));
|
||||
}
|
||||
|
||||
/** Shows or hides the close button (shown by default). */
|
||||
public GridStackItem setClosable(boolean closable) {
|
||||
if (closable == this.closable) {
|
||||
return this;
|
||||
}
|
||||
this.closable = closable;
|
||||
if (closable) {
|
||||
getElement().appendChild(closeButton.getElement());
|
||||
} else {
|
||||
closeButton.getElement().removeFromParent();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isClosable() {
|
||||
return closable;
|
||||
}
|
||||
|
||||
/** The close button element. Exposed for restyling, the same way
|
||||
* {@link #getDragHandle()} is. */
|
||||
public Div getCloseButton() {
|
||||
return closeButton;
|
||||
}
|
||||
|
||||
public Registration addCloseListener(ComponentEventListener<CloseEvent> listener) {
|
||||
return addListener(CloseEvent.class, listener);
|
||||
}
|
||||
|
||||
/** Fired after the item has been removed from its {@link GridStackLayout}. */
|
||||
public static class CloseEvent extends ComponentEvent<GridStackItem> {
|
||||
CloseEvent(GridStackItem source, boolean fromClient) {
|
||||
super(source, fromClient);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(Component... components) {
|
||||
|
||||
Reference in New Issue
Block a user