fix: make dashboard widgets closable (#14) #17
@@ -25,7 +25,8 @@ public enum Fa {
|
|||||||
ADD("fa-solid", "fa-plus"),
|
ADD("fa-solid", "fa-plus"),
|
||||||
REMOVE("fa-solid", "fa-trash"),
|
REMOVE("fa-solid", "fa-trash"),
|
||||||
RESET("fa-solid", "fa-arrow-rotate-left"),
|
RESET("fa-solid", "fa-arrow-rotate-left"),
|
||||||
DRAG("fa-solid", "fa-grip-vertical");
|
DRAG("fa-solid", "fa-grip-vertical"),
|
||||||
|
CLOSE("fa-solid", "fa-xmark");
|
||||||
|
|
||||||
private final String[] classes;
|
private final String[] classes;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.example.components;
|
package com.example.components;
|
||||||
|
|
||||||
import com.vaadin.flow.component.Component;
|
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.component.html.Div;
|
||||||
|
import com.vaadin.flow.shared.Registration;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -12,7 +15,10 @@ import java.util.UUID;
|
|||||||
* <p>
|
* <p>
|
||||||
* Dragging starts from the grip handle only, never from the item body, so the
|
* Dragging starts from the grip handle only, never from the item body, so the
|
||||||
* components inside an item stay interactive (see
|
* 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 {
|
public class GridStackItem extends Div {
|
||||||
|
|
||||||
@@ -20,8 +26,14 @@ public class GridStackItem extends Div {
|
|||||||
* {@link GridStackLayout}'s {@code handle} option. */
|
* {@link GridStackLayout}'s {@code handle} option. */
|
||||||
public static final String DRAG_HANDLE_CLASS = "dialect-drag-handle";
|
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 content = new Div();
|
||||||
private final Div dragHandle = new Div();
|
private final Div dragHandle = new Div();
|
||||||
|
private final Div closeButton = new Div();
|
||||||
|
private boolean closable = true;
|
||||||
|
|
||||||
public GridStackItem(Component... content) {
|
public GridStackItem(Component... content) {
|
||||||
this(UUID.randomUUID().toString(), 0, 0, 4, 3, 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("title", label);
|
||||||
dragHandle.getElement().setAttribute("aria-label", label);
|
dragHandle.getElement().setAttribute("aria-label", label);
|
||||||
getElement().appendChild(dragHandle.getElement());
|
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) {
|
public void add(Component... components) {
|
||||||
|
|||||||
@@ -18,14 +18,13 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
|||||||
import com.vaadin.flow.router.HasDynamicTitle;
|
import com.vaadin.flow.router.HasDynamicTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Showcase for {@link GridStackLayout}: a dashboard of draggable/resizable
|
* Showcase for {@link GridStackLayout}: a dashboard of draggable/resizable
|
||||||
* cards whose layout is persisted per browser, plus controls to add, remove
|
* cards whose layout is persisted per browser, plus controls to add widgets and
|
||||||
* and reset widgets at runtime.
|
* reset the layout at runtime. Widgets are removed by their own close button
|
||||||
|
* (see {@link GridStackItem#setClosable(boolean)}), not from the toolbar.
|
||||||
*/
|
*/
|
||||||
@Route("gridstack")
|
@Route("gridstack")
|
||||||
public class GridStackView extends VerticalLayout implements HasDynamicTitle {
|
public class GridStackView extends VerticalLayout implements HasDynamicTitle {
|
||||||
@@ -33,7 +32,6 @@ public class GridStackView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
private static final String STORAGE_KEY = "gridstack-demo";
|
private static final String STORAGE_KEY = "gridstack-demo";
|
||||||
|
|
||||||
private final GridStackLayout grid = new GridStackLayout();
|
private final GridStackLayout grid = new GridStackLayout();
|
||||||
private final Deque<GridStackItem> extraWidgets = new ArrayDeque<>();
|
|
||||||
private final Span status = new Span();
|
private final Span status = new Span();
|
||||||
private int extraWidgetCount;
|
private int extraWidgetCount;
|
||||||
|
|
||||||
@@ -67,13 +65,10 @@ public class GridStackView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
e -> addWidget());
|
e -> addWidget());
|
||||||
add.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
add.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
Button remove = new Button(getTranslation("gridstack.removeWidget"), Fa.REMOVE.create(),
|
|
||||||
e -> removeWidget());
|
|
||||||
|
|
||||||
Button reset = new Button(getTranslation("gridstack.reset"), Fa.RESET.create(),
|
Button reset = new Button(getTranslation("gridstack.reset"), Fa.RESET.create(),
|
||||||
e -> grid.resetLayout());
|
e -> grid.resetLayout());
|
||||||
|
|
||||||
HorizontalLayout toolbar = new HorizontalLayout(add, remove, reset, status);
|
HorizontalLayout toolbar = new HorizontalLayout(add, reset, status);
|
||||||
toolbar.setPadding(false);
|
toolbar.setPadding(false);
|
||||||
toolbar.setWidthFull();
|
toolbar.setWidthFull();
|
||||||
toolbar.setAlignItems(Alignment.CENTER);
|
toolbar.setAlignItems(Alignment.CENTER);
|
||||||
@@ -83,21 +78,13 @@ public class GridStackView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
private void addWidget() {
|
private void addWidget() {
|
||||||
// The id must stay stable across reloads for the saved layout to match
|
// The id must stay stable across reloads for the saved layout to match
|
||||||
// it again, so it is derived from a counter rather than a random UUID.
|
// it again, so it is derived from a counter rather than a random UUID.
|
||||||
|
// The counter only ever grows: closing a widget must not hand its id to
|
||||||
|
// the next one, or the new widget would inherit the closed one's saved
|
||||||
|
// position.
|
||||||
extraWidgetCount++;
|
extraWidgetCount++;
|
||||||
String title = getTranslation("gridstack.widget", extraWidgetCount);
|
String title = getTranslation("gridstack.widget", extraWidgetCount);
|
||||||
GridStackItem item = new GridStackItem("extra-" + extraWidgetCount, 0, 0, 4, 2,
|
grid.add(new GridStackItem("extra-" + extraWidgetCount, 0, 0, 4, 2,
|
||||||
new Card(title, new Paragraph(getTranslation("gridstack.widgetText"))));
|
new Card(title, new Paragraph(getTranslation("gridstack.widgetText")))));
|
||||||
extraWidgets.push(item);
|
|
||||||
grid.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeWidget() {
|
|
||||||
GridStackItem item = extraWidgets.poll();
|
|
||||||
if (item == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
grid.remove(item);
|
|
||||||
extraWidgetCount--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Component lineChart() {
|
private Component lineChart() {
|
||||||
|
|||||||
@@ -176,9 +176,45 @@ vaadin-app-layout::part(content) {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Touch devices never hover — keep the grip permanently visible there. */
|
/* Close button (GridStackItem#setClosable): same affordance as the grip, in
|
||||||
|
the outermost corner slot. The grip steps aside only when it is there, so a
|
||||||
|
non-closable item keeps its grip flush with the corner. */
|
||||||
|
.dialect-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--dialect-ink);
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-stack-item:has(> .dialect-close-button) > .dialect-drag-handle {
|
||||||
|
right: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-stack-item:hover > .dialect-close-button,
|
||||||
|
.dialect-close-button:focus-visible {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialect-close-button:hover,
|
||||||
|
.dialect-close-button:active {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--dialect-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch devices never hover — keep the corner controls permanently visible. */
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
.dialect-drag-handle {
|
.dialect-drag-handle,
|
||||||
|
.dialect-close-button {
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ card.registration=Registrierung
|
|||||||
card.employees=Mitarbeiter
|
card.employees=Mitarbeiter
|
||||||
card.gridstackHint=Bedienung
|
card.gridstackHint=Bedienung
|
||||||
|
|
||||||
gridstack.hint=Karten am Griff oben rechts verschieben, an der unteren rechten Ecke die Größe ändern. Das Layout wird im Browser gespeichert und beim nächsten Besuch wiederhergestellt.
|
gridstack.hint=Karten am Griff oben rechts verschieben, an der unteren rechten Ecke die Größe ändern, mit dem X oben rechts schließen. Das Layout wird im Browser gespeichert und beim nächsten Besuch wiederhergestellt.
|
||||||
gridstack.addWidget=Widget hinzufügen
|
gridstack.addWidget=Widget hinzufügen
|
||||||
gridstack.removeWidget=Letztes entfernen
|
|
||||||
gridstack.reset=Layout zurücksetzen
|
gridstack.reset=Layout zurücksetzen
|
||||||
gridstack.status=Layout geändert – {0} Widgets
|
gridstack.status=Layout geändert – {0} Widgets
|
||||||
gridstack.statusInitial=Layout unverändert
|
gridstack.statusInitial=Layout unverändert
|
||||||
gridstack.widget=Widget {0}
|
gridstack.widget=Widget {0}
|
||||||
gridstack.widgetText=Frei platzierbare Karte.
|
gridstack.widgetText=Frei platzierbare Karte.
|
||||||
gridstack.dragHandle=Verschieben
|
gridstack.dragHandle=Verschieben
|
||||||
|
gridstack.close=Schließen
|
||||||
|
|
||||||
chart.revenueSeries=Umsatz 2026
|
chart.revenueSeries=Umsatz 2026
|
||||||
chart.pointClick=Serie {0}, Punkt {1}
|
chart.pointClick=Serie {0}, Punkt {1}
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ card.registration=Registration
|
|||||||
card.employees=Employees
|
card.employees=Employees
|
||||||
card.gridstackHint=How it works
|
card.gridstackHint=How it works
|
||||||
|
|
||||||
gridstack.hint=Move cards with the grip in their top right corner, resize them from the bottom right corner. The layout is stored in your browser and restored on your next visit.
|
gridstack.hint=Move cards with the grip in their top right corner, resize them from the bottom right corner, close them with the X in the top right corner. The layout is stored in your browser and restored on your next visit.
|
||||||
gridstack.addWidget=Add widget
|
gridstack.addWidget=Add widget
|
||||||
gridstack.removeWidget=Remove last
|
|
||||||
gridstack.reset=Reset layout
|
gridstack.reset=Reset layout
|
||||||
gridstack.status=Layout changed – {0} widgets
|
gridstack.status=Layout changed – {0} widgets
|
||||||
gridstack.statusInitial=Layout unchanged
|
gridstack.statusInitial=Layout unchanged
|
||||||
gridstack.widget=Widget {0}
|
gridstack.widget=Widget {0}
|
||||||
gridstack.widgetText=Freely placeable card.
|
gridstack.widgetText=Freely placeable card.
|
||||||
gridstack.dragHandle=Move
|
gridstack.dragHandle=Move
|
||||||
|
gridstack.close=Close
|
||||||
|
|
||||||
chart.revenueSeries=Revenue 2026
|
chart.revenueSeries=Revenue 2026
|
||||||
chart.pointClick=Series {0}, point {1}
|
chart.pointClick=Series {0}, point {1}
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ card.registration=Registro
|
|||||||
card.employees=Empleados
|
card.employees=Empleados
|
||||||
card.gridstackHint=Cómo funciona
|
card.gridstackHint=Cómo funciona
|
||||||
|
|
||||||
gridstack.hint=Mueve las tarjetas con el asa de la esquina superior derecha y cambia su tamaño desde la esquina inferior derecha. El diseño se guarda en el navegador y se restaura en la próxima visita.
|
gridstack.hint=Mueve las tarjetas con el asa de la esquina superior derecha, cambia su tamaño desde la esquina inferior derecha y ciérralas con la X de la esquina superior derecha. El diseño se guarda en el navegador y se restaura en la próxima visita.
|
||||||
gridstack.addWidget=Añadir widget
|
gridstack.addWidget=Añadir widget
|
||||||
gridstack.removeWidget=Quitar el último
|
|
||||||
gridstack.reset=Restablecer diseño
|
gridstack.reset=Restablecer diseño
|
||||||
gridstack.status=Diseño modificado – {0} widgets
|
gridstack.status=Diseño modificado – {0} widgets
|
||||||
gridstack.statusInitial=Diseño sin cambios
|
gridstack.statusInitial=Diseño sin cambios
|
||||||
gridstack.widget=Widget {0}
|
gridstack.widget=Widget {0}
|
||||||
gridstack.widgetText=Tarjeta de colocación libre.
|
gridstack.widgetText=Tarjeta de colocación libre.
|
||||||
gridstack.dragHandle=Mover
|
gridstack.dragHandle=Mover
|
||||||
|
gridstack.close=Cerrar
|
||||||
|
|
||||||
chart.revenueSeries=Ingresos 2026
|
chart.revenueSeries=Ingresos 2026
|
||||||
chart.pointClick=Serie {0}, punto {1}
|
chart.pointClick=Serie {0}, punto {1}
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ import com.example.components.GridStackItem;
|
|||||||
import com.example.components.GridStackLayout;
|
import com.example.components.GridStackLayout;
|
||||||
import com.vaadin.browserless.SpringBrowserlessTest;
|
import com.vaadin.browserless.SpringBrowserlessTest;
|
||||||
import com.vaadin.browserless.ViewPackages;
|
import com.vaadin.browserless.ViewPackages;
|
||||||
|
import com.vaadin.browserless.internal.ElementUtilsKt;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.dom.DomEvent;
|
||||||
|
import com.vaadin.flow.internal.JacksonUtils;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@@ -32,7 +37,7 @@ class GridStackViewTest extends SpringBrowserlessTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addAndRemoveWidget_changesItemCount() {
|
void addAndCloseWidget_changesItemCount() {
|
||||||
navigate(GridStackView.class);
|
navigate(GridStackView.class);
|
||||||
|
|
||||||
GridStackLayout grid = $view(GridStackLayout.class).first();
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||||
@@ -41,18 +46,59 @@ class GridStackViewTest extends SpringBrowserlessTest {
|
|||||||
button("gridstack.addWidget").click();
|
button("gridstack.addWidget").click();
|
||||||
assertEquals(initial + 1, grid.getLayout().size());
|
assertEquals(initial + 1, grid.getLayout().size());
|
||||||
|
|
||||||
button("gridstack.removeWidget").click();
|
clickCloseButton($view(GridStackItem.class).last());
|
||||||
assertEquals(initial, grid.getLayout().size());
|
assertEquals(initial, grid.getLayout().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void removeWidget_onDefaultLayout_keepsStaticWidgets() {
|
void closeButton_removesTheClickedWidgetOnly() {
|
||||||
navigate(GridStackView.class);
|
navigate(GridStackView.class);
|
||||||
|
|
||||||
GridStackLayout grid = $view(GridStackLayout.class).first();
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||||
button("gridstack.removeWidget").click();
|
GridStackItem first = $view(GridStackItem.class).first();
|
||||||
|
String closedId = first.getItemId();
|
||||||
|
|
||||||
assertEquals(4, grid.getLayout().size(), "static widgets must not be removable");
|
clickCloseButton(first);
|
||||||
|
|
||||||
|
List<GridStackItem.Position> layout = grid.getLayout();
|
||||||
|
assertEquals(3, layout.size());
|
||||||
|
assertFalse(layout.stream().anyMatch(p -> closedId.equals(p.id())),
|
||||||
|
"the closed widget must be gone from the layout");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closeListener_firesOnceItemIsDetached() {
|
||||||
|
navigate(GridStackView.class);
|
||||||
|
|
||||||
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||||
|
GridStackItem item = $view(GridStackItem.class).first();
|
||||||
|
int[] fired = {0};
|
||||||
|
item.addCloseListener(e -> fired[0]++);
|
||||||
|
|
||||||
|
item.close();
|
||||||
|
|
||||||
|
assertEquals(1, fired[0]);
|
||||||
|
assertEquals(3, grid.getLayout().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonClosableItem_hasNoCloseButton() {
|
||||||
|
navigate(GridStackView.class);
|
||||||
|
|
||||||
|
GridStackItem item = $view(GridStackItem.class).first();
|
||||||
|
assertTrue(item.isClosable(), "items are closable by default");
|
||||||
|
|
||||||
|
item.setClosable(false);
|
||||||
|
assertFalse(item.isClosable());
|
||||||
|
assertFalse(item.getChildren().anyMatch(child -> child == item.getCloseButton()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fires the DOM click the close button listens for, rather than calling
|
||||||
|
* {@code close()} directly, so the button's own wiring is covered too. */
|
||||||
|
private void clickCloseButton(GridStackItem item) {
|
||||||
|
Div closeButton = item.getCloseButton();
|
||||||
|
ElementUtilsKt._fireDomEvent(closeButton.getElement(),
|
||||||
|
new DomEvent(closeButton.getElement(), "click", JacksonUtils.createObjectNode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Button button(String translationKey) {
|
private Button button(String translationKey) {
|
||||||
|
|||||||
Reference in New Issue
Block a user