Skip to content

Commit 8b00ed7

Browse files
ugur-vaadinvursen
andauthored
feat: implement disable on click for menu item (#6815)
* feat: implement disable on click for menu item * fix: defer setting disabled * test: add unit tests for controller * test: fix test class * fix: check if element is there on disableonclick init and cleanup * test: fix button tests * Update vaadin-flow-components-shared-parent/vaadin-flow-components-base/src/main/java/com/vaadin/flow/component/shared/internal/DisableOnClickController.java Co-authored-by: Sergey Vinogradov <[email protected]> * Update vaadin-flow-components-shared-parent/vaadin-flow-components-base/src/main/java/com/vaadin/flow/component/shared/internal/DisableOnClickController.java Co-authored-by: Sergey Vinogradov <[email protected]> * Update vaadin-flow-components-shared-parent/vaadin-flow-components-base/src/main/java/com/vaadin/flow/component/shared/internal/DisableOnClickController.java Co-authored-by: Sergey Vinogradov <[email protected]> * refactor: update disable on click init logic and add tests * refactor: rename and return early when initializing * test: reuse elements and simplify disabled asseertion --------- Co-authored-by: Sergey Vinogradov <[email protected]>
1 parent 6551f2e commit 8b00ed7

File tree

14 files changed

+794
-160
lines changed

14 files changed

+794
-160
lines changed

vaadin-button-flow-parent/vaadin-button-flow-integration-tests/src/main/java/com/vaadin/flow/component/button/tests/ButtonView.java

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public ButtonView() {
4949
createButtonsWithTabIndex();
5050
createDisabledButton();
5151
createButtonWithDisableOnClick();
52-
createButtonWithDisableOnClickThatEnablesInSameRoundtrip();
52+
createButtonWithDisableOnClickThatEnablesInSameRoundTrip();
5353
createButtonWithDisableOnClickThatIsHidden();
5454
createButtonWithDisableOnClickAndPointerEventsAuto();
5555
addVariantsFeature();
@@ -207,20 +207,6 @@ private void createButtonWithDisableOnClick() {
207207
});
208208
disableOnClickButton.setDisableOnClick(true);
209209

210-
Button temporarilyDisabledButton = new Button(
211-
"Temporarily disabled button", event -> {
212-
try {
213-
// Blocking the user from clicking the button
214-
// multiple times, due to a long running request that
215-
// is not running asynchronously.
216-
Thread.sleep(1500);
217-
event.getSource().setEnabled(true);
218-
} catch (InterruptedException e) {
219-
Thread.currentThread().interrupt();
220-
}
221-
});
222-
temporarilyDisabledButton.setDisableOnClick(true);
223-
224210
final Div disabledMessage = new Div();
225211
disabledMessage.setId("disabled-message");
226212

@@ -240,7 +226,7 @@ private void createButtonWithDisableOnClick() {
240226
toggle.setId("toggle-button");
241227

242228
addCard("Button disabled on click", disableOnClickButton, enable,
243-
toggle, disabledMessage, new Div(temporarilyDisabledButton));
229+
toggle, disabledMessage);
244230

245231
disableOnClickButton.addClickListener(evt -> disabledMessage
246232
.setText("Button " + evt.getSource().getText()
@@ -249,14 +235,23 @@ private void createButtonWithDisableOnClick() {
249235
+ runCount.incrementAndGet() + " clicks"));
250236

251237
disableOnClickButton.setId("disable-on-click-button");
252-
temporarilyDisabledButton.setId("temporarily-disabled-button");
253238
enable.setId("enable-button");
254239
}
255240

256-
private void createButtonWithDisableOnClickThatEnablesInSameRoundtrip() {
241+
private void createButtonWithDisableOnClickThatEnablesInSameRoundTrip() {
257242
Button button = new Button(
258-
"Disabled on click and re-enabled in same roundtrip", event -> {
259-
event.getSource().setEnabled(true);
243+
"Disabled on click and re-enabled in same round-trip",
244+
event -> {
245+
try {
246+
// Blocking the user from clicking the button
247+
// multiple times, due to a long-running request that
248+
// is not running asynchronously.
249+
Thread.sleep(500);
250+
} catch (InterruptedException e) {
251+
throw new RuntimeException(e);
252+
} finally {
253+
event.getSource().setEnabled(true);
254+
}
260255
});
261256
button.setDisableOnClick(true);
262257
button.setId("disable-on-click-re-enable-button");

vaadin-button-flow-parent/vaadin-button-flow-integration-tests/src/test/java/com/vaadin/flow/component/button/tests/ButtonIT.java

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public void textContains() {
6868

6969
buttonElements = $(ButtonElement.class).withTextContaining("button")
7070
.all();
71-
Assert.assertEquals(4, buttonElements.size());
71+
Assert.assertEquals(3, buttonElements.size());
7272

7373
buttonElements = $(ButtonElement.class)
7474
.withTextContaining("nonexistent").all();
@@ -83,11 +83,11 @@ public void textBiPredicate() {
8383

8484
buttonElements = $(ButtonElement.class)
8585
.withText("button", String::endsWith).all();
86-
Assert.assertEquals(3, buttonElements.size());
86+
Assert.assertEquals(2, buttonElements.size());
8787

8888
buttonElements = $(ButtonElement.class)
8989
.withText("button", ButtonIT::containsIgnoreCase).all();
90-
Assert.assertEquals(5, buttonElements.size());
90+
Assert.assertEquals(4, buttonElements.size());
9191
}
9292

9393
@Test
@@ -109,7 +109,7 @@ public void captionContains() {
109109

110110
buttonElements = $(ButtonElement.class).withCaptionContaining("button")
111111
.all();
112-
Assert.assertEquals(4, buttonElements.size());
112+
Assert.assertEquals(3, buttonElements.size());
113113

114114
buttonElements = $(ButtonElement.class)
115115
.withCaptionContaining("nonexistent").all();
@@ -290,29 +290,8 @@ public void clickDisableOnClickButton_newClickNotRegistered() {
290290
}
291291
}
292292

293-
@Test // https://github.com/vaadin/vaadin-button-flow/issues/115
294-
public void disableButtonOnClick_canBeEnabled() {
295-
getCommandExecutor().disableWaitForVaadin();
296-
ButtonElement button = $(ButtonElement.class)
297-
.id("temporarily-disabled-button");
298-
299-
for (int i = 0; i < 3; i++) {
300-
button.click();
301-
302-
Assert.assertFalse("button should be disabled", button.isEnabled());
303-
waitUntil(ExpectedConditions.elementToBeClickable(
304-
$(ButtonElement.class).id("temporarily-disabled-button")),
305-
2000);
306-
307-
Assert.assertTrue("button should be enabled again",
308-
button.isEnabled());
309-
}
310-
311-
getCommandExecutor().enableWaitForVaadin();
312-
}
313-
314293
@Test
315-
public void removeDisabled_buttonWorksNormally() {
294+
public void removeDisableOnClick_buttonWorksNormally() {
316295
WebElement button = layout
317296
.findElement(By.id("disable-on-click-button"));
318297
Assert.assertTrue(
@@ -347,17 +326,18 @@ public void removeDisabled_buttonWorksNormally() {
347326
}
348327

349328
@Test
350-
public void disableOnClick_enableInSameRoundtrip_clientSideButtonIsEnabled() {
351-
WebElement button = layout
352-
.findElement(By.id("disable-on-click-re-enable-button"));
329+
public void disableOnClick_enableInSameRoundTrip_clientSideButtonIsEnabled() {
330+
var itemId = "disable-on-click-re-enable-button";
331+
getCommandExecutor().disableWaitForVaadin();
332+
var button = findElement(By.id(itemId));
353333
for (int i = 0; i < 3; i++) {
354-
Boolean disabled = (Boolean) executeScript(
355-
"arguments[0].click(); return arguments[0].disabled",
356-
button);
357-
Assert.assertTrue(disabled);
358-
359-
waitUntil(ExpectedConditions.elementToBeClickable(button));
334+
button.click();
335+
getCommandExecutor().getDriver()
336+
.executeAsyncScript("requestAnimationFrame(arguments[0])");
337+
Assert.assertFalse(button.isEnabled());
338+
waitUntil(driver -> button.isEnabled());
360339
}
340+
getCommandExecutor().enableWaitForVaadin();
361341
}
362342

363343
@Test

vaadin-button-flow-parent/vaadin-button-flow-integration-tests/src/test/java/com/vaadin/flow/component/button/tests/DetachReattachDisableOnClickButtonIT.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ private void clickDisableOnClickButton(ButtonElement disableOnClickButton) {
5050
disableOnClickButton.click();
5151

5252
// Check 'Disable on click' button is disabled
53-
assertDisableOnClickButtonDisabled(disableOnClickButton);
53+
waitUntil(driver -> !$(ButtonElement.class).id("disable-on-click")
54+
.isEnabled(), 2);
5455

5556
waitUntil(ExpectedConditions.elementToBeClickable(
56-
$(ButtonElement.class).id("disable-on-click")), 2000);
57+
$(ButtonElement.class).id("disable-on-click")), 2);
5758

5859
// Check 'Disable on click' button is enabled again
5960
assertDisableOnClickButtonEnabled(disableOnClickButton);
@@ -76,15 +77,15 @@ public void testDetachingAndReattachingShouldKeepDisabledOnClick() {
7677
removeFromViewButton.click();
7778

7879
waitUntil(ExpectedConditions
79-
.numberOfElementsToBe(By.id("disable-on-click"), 0), 2000);
80+
.numberOfElementsToBe(By.id("disable-on-click"), 0), 2);
8081

8182
// Re-attach 'Disable on click" button
8283
ButtonElement addToViewButton = $(ButtonElement.class)
8384
.id("add-to-view");
8485
addToViewButton.click();
8586

8687
waitUntil(ExpectedConditions
87-
.numberOfElementsToBe(By.id("disable-on-click"), 1), 2000);
88+
.numberOfElementsToBe(By.id("disable-on-click"), 1), 2);
8889

8990
disableOnClickButton = getDisableOnClickButton();
9091

vaadin-button-flow-parent/vaadin-button-flow/src/main/java/com/vaadin/flow/component/button/Button.java

Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.stream.Collectors;
2121
import java.util.stream.Stream;
2222

23-
import com.vaadin.flow.component.AttachEvent;
2423
import com.vaadin.flow.component.ClickEvent;
2524
import com.vaadin.flow.component.ClickNotifier;
2625
import com.vaadin.flow.component.Component;
@@ -35,13 +34,12 @@
3534
import com.vaadin.flow.component.dependency.JsModule;
3635
import com.vaadin.flow.component.dependency.NpmPackage;
3736
import com.vaadin.flow.component.html.Image;
38-
import com.vaadin.flow.component.page.PendingJavaScriptResult;
3937
import com.vaadin.flow.component.shared.HasPrefix;
4038
import com.vaadin.flow.component.shared.HasSuffix;
4139
import com.vaadin.flow.component.shared.HasThemeVariant;
4240
import com.vaadin.flow.component.shared.HasTooltip;
41+
import com.vaadin.flow.component.shared.internal.DisableOnClickController;
4342
import com.vaadin.flow.dom.Element;
44-
import com.vaadin.flow.shared.Registration;
4543

4644
/**
4745
* The Button component allows users to perform actions. It comes in several
@@ -54,24 +52,15 @@
5452
@JsModule("@vaadin/polymer-legacy-adapter/style-modules.js")
5553
@NpmPackage(value = "@vaadin/button", version = "24.6.0-alpha8")
5654
@JsModule("@vaadin/button/src/vaadin-button.js")
57-
@JsModule("./buttonFunctions.js")
5855
public class Button extends Component
5956
implements ClickNotifier<Button>, Focusable<Button>, HasAriaLabel,
6057
HasEnabled, HasPrefix, HasSize, HasStyle, HasSuffix, HasText,
6158
HasThemeVariant<ButtonVariant>, HasTooltip {
6259

6360
private Component iconComponent;
6461
private boolean iconAfterText;
65-
private boolean disableOnClick = false;
66-
private PendingJavaScriptResult initDisableOnClick;
67-
68-
// Register immediately as first listener
69-
private final Registration disableListener = addClickListener(
70-
buttonClickEvent -> {
71-
if (disableOnClick) {
72-
setEnabled(false);
73-
}
74-
});
62+
private final DisableOnClickController<Button> disableOnClickController = new DisableOnClickController<>(
63+
this);
7564

7665
/**
7766
* Default constructor. Creates an empty button.
@@ -330,55 +319,32 @@ public boolean isAutofocus() {
330319
}
331320

332321
/**
333-
* Set the button so that it is disabled on click.
322+
* Sets whether the button should be disabled when clicked.
334323
* <p>
335-
* Enabling the button needs to happen from the server.
324+
* When set to {@code true}, the button will be immediately disabled on the
325+
* client-side when clicked, preventing further clicks until re-enabled from
326+
* the server-side.
336327
*
337328
* @param disableOnClick
338-
* true to disable button immediately when clicked
329+
* whether the button should be disabled when clicked
339330
*/
340331
public void setDisableOnClick(boolean disableOnClick) {
341-
this.disableOnClick = disableOnClick;
342-
if (disableOnClick) {
343-
getElement().setAttribute("disableOnClick", "true");
344-
initDisableOnClick();
345-
} else {
346-
getElement().removeAttribute("disableOnClick");
347-
}
332+
disableOnClickController.setDisableOnClick(disableOnClick);
348333
}
349334

350335
/**
351-
* Get if button is set to be disabled on click.
336+
* Gets whether the button is set to be disabled when clicked.
352337
*
353-
* @return {@code true} if button gets disabled on click, else {@code false}
338+
* @return whether button is set to be disabled on click
354339
*/
355340
public boolean isDisableOnClick() {
356-
return disableOnClick;
357-
}
358-
359-
/**
360-
* Initialize client side disabling so disabled if immediate on click even
361-
* if server-side handling takes some time.
362-
*/
363-
private void initDisableOnClick() {
364-
if (initDisableOnClick == null) {
365-
initDisableOnClick = getElement().executeJs(
366-
"window.Vaadin.Flow.button.initDisableOnClick($0)");
367-
getElement().getNode()
368-
.runWhenAttached(ui -> ui.beforeClientResponse(this,
369-
executionContext -> this.initDisableOnClick = null));
370-
}
341+
return disableOnClickController.isDisableOnClick();
371342
}
372343

373344
@Override
374345
public void setEnabled(boolean enabled) {
375346
Focusable.super.setEnabled(enabled);
376-
// Force updating the disabled state on the client
377-
// When using disable on click, the client side will immediately
378-
// run JS to disable the button. If the button is then disabled and
379-
// re-enabled during the same round trip, Flow will not detect any
380-
// changes and the client side button would not be enabled again.
381-
getElement().executeJs("this.disabled = $0", !enabled);
347+
disableOnClickController.onSetEnabled(enabled);
382348
}
383349

384350
private void updateIconSlot() {
@@ -444,11 +410,4 @@ private void updateThemeAttribute() {
444410
getThemeNames().remove("icon");
445411
}
446412
}
447-
448-
@Override
449-
protected void onAttach(AttachEvent attachEvent) {
450-
if (isDisableOnClick()) {
451-
initDisableOnClick();
452-
}
453-
}
454413
}

vaadin-button-flow-parent/vaadin-button-flow/src/main/resources/META-INF/resources/frontend/buttonFunctions.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

vaadin-button-flow-parent/vaadin-button-flow/src/test/java/com/vaadin/flow/component/button/tests/ButtonTest.java

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,19 @@
1515
*/
1616
package com.vaadin.flow.component.button.tests;
1717

18-
import java.util.Optional;
1918
import java.util.Set;
2019
import java.util.concurrent.atomic.AtomicBoolean;
2120

2221
import org.junit.Assert;
2322
import org.junit.Test;
24-
import org.mockito.Mockito;
2523

2624
import com.vaadin.flow.component.HasAriaLabel;
2725
import com.vaadin.flow.component.Text;
2826
import com.vaadin.flow.component.button.Button;
2927
import com.vaadin.flow.component.button.ButtonVariant;
3028
import com.vaadin.flow.component.icon.Icon;
3129
import com.vaadin.flow.component.icon.VaadinIcon;
32-
import com.vaadin.flow.component.internal.PendingJavaScriptInvocation;
3330
import com.vaadin.flow.component.shared.HasTooltip;
34-
import com.vaadin.flow.dom.Element;
35-
import com.vaadin.flow.internal.StateNode;
3631

3732
public class ButtonTest {
3833

@@ -342,28 +337,6 @@ public void setAriaLabel() {
342337
Assert.assertEquals("Aria label", button.getAriaLabel().get());
343338
}
344339

345-
@Test
346-
public void initDisableOnClick_onlyCalledOnceForSeverRoundtrip() {
347-
final Element element = Mockito.mock(Element.class);
348-
StateNode node = new StateNode();
349-
button = Mockito.spy(Button.class);
350-
351-
Mockito.when(button.getElement()).thenReturn(element);
352-
353-
Mockito.when(element.executeJs(Mockito.anyString()))
354-
.thenReturn(Mockito.mock(PendingJavaScriptInvocation.class));
355-
Mockito.when(element.getComponent()).thenReturn(Optional.of(button));
356-
Mockito.when(element.getParent()).thenReturn(null);
357-
Mockito.when(element.getNode()).thenReturn(node);
358-
359-
button.setDisableOnClick(true);
360-
button.setDisableOnClick(false);
361-
button.setDisableOnClick(true);
362-
363-
Mockito.verify(element, Mockito.times(1))
364-
.executeJs("window.Vaadin.Flow.button.initDisableOnClick($0)");
365-
}
366-
367340
private void assertButtonHasThemeAttribute(String theme) {
368341
Assert.assertTrue("Expected " + theme + " to be in the theme attribute",
369342
button.getThemeNames().contains(theme));
@@ -388,9 +361,4 @@ private void assertIconAfterText() {
388361
Assert.assertTrue(button.isIconAfterText());
389362
Assert.assertEquals("suffix", icon.getElement().getAttribute("slot"));
390363
}
391-
392-
private Element getButtonChild(int index) {
393-
return button.getElement().getChild(index);
394-
}
395-
396364
}

0 commit comments

Comments
 (0)