Skip to content

Commit

Permalink
feat: add Popover opened state and related API (#6337)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored Jul 11, 2024
1 parent eb63f4c commit c073c7a
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ public PopoverView() {
event -> add(target));
attachTarget.setId("attach-target");

add(popover, clearTarget, detachTarget, attachTarget, target);
NativeButton disableCloseOnEsc = new NativeButton(
"Disable close on Esc", event -> popover.setCloseOnEsc(false));
disableCloseOnEsc.setId("disable-close-on-esc");

NativeButton disableCloseOnOutsideClick = new NativeButton(
"Disable close on outside click",
event -> popover.setCloseOnOutsideClick(false));
disableCloseOnOutsideClick.setId("disable-close-on-outside-click");

add(popover, clearTarget, detachTarget, attachTarget, disableCloseOnEsc,
disableCloseOnOutsideClick, target);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package com.vaadin.flow.component.popover.tests;

import com.vaadin.flow.component.popover.testbench.PopoverElement;
import com.vaadin.flow.testutil.TestPath;
import com.vaadin.tests.AbstractComponentIT;

Expand All @@ -24,7 +25,9 @@
import org.junit.Test;

import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;

/**
* Integration tests for the {@link PopoverView}.
Expand All @@ -36,18 +39,23 @@ public class PopoverIT extends AbstractComponentIT {

static final String POPOVER_OVERLAY_TAG = "vaadin-popover-overlay";

PopoverElement popover;

@Before
public void init() {
open();
popover = $(PopoverElement.class).first();
}

@Test
public void clickTarget_popoverOpensAndCloses() {
clickTarget();
checkPopoverIsOpened();
Assert.assertTrue(popover.isOpen());

clickTarget();
checkPopoverIsClosed();
Assert.assertFalse(popover.isOpen());
}

@Test
Expand Down Expand Up @@ -88,6 +96,46 @@ public void detachTarget_clearAndReattach_clickTarget_popoverDoesNotOpen() {
checkPopoverIsClosed();
}

@Test
public void clickOutside_popoverCloses() {
clickTarget();
checkPopoverIsOpened();

$("body").first().click();
Assert.assertFalse(popover.isOpen());
}

@Test
public void disableCloseOnOutsideClick_clickOutside_popoverDoesNotClose() {
clickElementWithJs("disable-close-on-outside-click");

clickTarget();
checkPopoverIsOpened();

$("body").first().click();
Assert.assertTrue(popover.isOpen());
}

@Test
public void pressEsc_popoverCloses() {
clickTarget();
checkPopoverIsOpened();

new Actions(getDriver()).sendKeys(Keys.ESCAPE).build().perform();
Assert.assertFalse(popover.isOpen());
}

@Test
public void disableCloseOnEsc_pressEsc_popoverDoesNotClose() {
clickElementWithJs("disable-close-on-esc");

clickTarget();
checkPopoverIsOpened();

new Actions(getDriver()).sendKeys(Keys.ESCAPE).build().perform();
Assert.assertTrue(popover.isOpen());
}

private void clickTarget() {
clickElementWithJs("popover-target");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.DomEvent;
import com.vaadin.flow.component.HasAriaLabel;
import com.vaadin.flow.component.HasComponents;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.UI;
Expand Down Expand Up @@ -70,10 +74,81 @@ public class Popover extends Component implements HasAriaLabel, HasComponents {
public Popover() {
getElement().getNode().addAttachListener(this::attachComponentRenderer);

// Workaround for: https://github.com/vaadin/flow/issues/3496
getElement().setProperty("opened", false);

updateTrigger();
setOverlayRole("dialog");
}


/**
* {@code opened-changed} event is sent when the overlay opened state
* changes.
*/
@DomEvent("opened-changed")
public static class OpenedChangeEvent extends ComponentEvent<Popover> {
private final boolean opened;

public OpenedChangeEvent(Popover source, boolean fromClient) {
super(source, fromClient);
this.opened = source.isOpened();
}

public boolean isOpened() {
return opened;
}
}

/**
* Opens or closes the popover.
*
* @param opened
* {@code true} to open the popover, {@code false} to close it
*/
public void setOpened(boolean opened) {
if (opened != isOpened()) {
getElement().setProperty("opened", opened);
fireEvent(new OpenedChangeEvent(this, false));
}
}

/**
* Opens the popover.
*/
public void open() {
setOpened(true);
}

/**
* Closes the popover.
*/
public void close() {
setOpened(false);
}

/**
* Gets the open state from the popover.
*
* @return the {@code opened} property from the popover
*/
@Synchronize(property = "opened", value = "opened-changed")
public boolean isOpened() {
return getElement().getProperty("opened", false);
}

/**
* Add a listener for event fired by the {@code opened-changed} events.
*
* @param listener
* the listener to add
* @return a Registration for removing the event listener
*/
public Registration addOpenedChangeListener(
ComponentEventListener<OpenedChangeEvent> listener) {
return addListener(OpenedChangeEvent.class, listener);
}

@Override
public void setAriaLabel(String ariaLabel) {
getElement().setProperty("accessibleName", ariaLabel);
Expand Down Expand Up @@ -117,6 +192,56 @@ public String getOverlayRole() {
return getElement().getProperty("overlayRole");
}

/**
* Gets whether this popover can be closed by pressing the Esc key or not.
* <p>
* By default, the popover is closable with Esc.
*
* @return {@code true} if this popover can be closed with the Esc key,
* {@code false} otherwise
*/
public boolean isCloseOnEsc() {
return !getElement().getProperty("noCloseOnEsc", false);
}

/**
* Sets whether this popover can be closed by pressing the Esc key or not.
* <p>
* By default, the popover is closable with Esc.
*
* @param closeOnEsc
* {@code true} to enable closing this popover with the Esc key,
* {@code false} to disable it
*/
public void setCloseOnEsc(boolean closeOnEsc) {
getElement().setProperty("noCloseOnEsc", !closeOnEsc);
}

/**
* Gets whether this popover can be closed by clicking outside of it or not.
* <p>
* By default, the popover is closable with an outside click.
*
* @return {@code true} if this popover can be closed by an outside click,
* {@code false} otherwise
*/
public boolean isCloseOnOutsideClick() {
return !getElement().getProperty("noCloseOnOutsideClick", false);
}

/**
* Sets whether this popover can be closed by clicking outside of it or not.
* <p>
* By default, the popover is closable with an outside click.
*
* @param closeOnOutsideClick
* {@code true} to enable closing this popover with an outside
* click, {@code false} to disable it
*/
public void setCloseOnOutsideClick(boolean closeOnOutsideClick) {
getElement().setProperty("noCloseOnOutsideClick", !closeOnOutsideClick);
}

/**
* Sets position of the popover with respect to its target.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2000-2024 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.popover;

import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.VaadinSession;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import java.util.concurrent.atomic.AtomicReference;

public class PopoverOpenedChangeListenerTest {
private final UI ui = new UI();
private Popover popover;
private AtomicReference<Popover.OpenedChangeEvent> event;
private ComponentEventListener<Popover.OpenedChangeEvent> mockListener;

@SuppressWarnings("unchecked")
@Before
public void setup() {
UI.setCurrent(ui);

VaadinSession session = Mockito.mock(VaadinSession.class);
Mockito.when(session.hasLock()).thenReturn(true);
ui.getInternals().setSession(session);

popover = new Popover();
ui.add(popover);

event = new AtomicReference<>();
popover.addOpenedChangeListener(event::set);

mockListener = Mockito.mock(ComponentEventListener.class);
popover.addOpenedChangeListener(mockListener);
}

@After
public void tearDown() {
UI.setCurrent(null);
}

@Test
public void open() {
popover.open();

Assert.assertFalse(event.get().isFromClient());
Assert.assertTrue(event.get().isOpened());
assertListenerCalls(1);

clearCapturedData();
popover.open();
Assert.assertNull(event.get());
assertListenerCalls(0);
}

@Test
public void close() {
popover.open();
clearCapturedData();

popover.close();
Assert.assertFalse(event.get().isFromClient());
Assert.assertFalse(event.get().isOpened());
assertListenerCalls(1);

clearCapturedData();
popover.close();
Assert.assertNull(event.get());
assertListenerCalls(0);
}

private void assertListenerCalls(int expectedCount) {
Mockito.verify(mockListener, Mockito.times(expectedCount))
.onComponentEvent(Mockito.any());
}

private void clearCapturedData() {
event.set(null);
Mockito.reset(mockListener);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.vaadin.flow.component.popover.testbench;

import org.openqa.selenium.SearchContext;
import org.openqa.selenium.StaleElementReferenceException;

import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elementsbase.Element;
Expand All @@ -32,4 +33,20 @@ public SearchContext getContext() {
// Find child elements inside the overlay,
return getPropertyElement("_overlayElement");
}

/**
* Checks whether the popover is shown.
*
* @return <code>true</code> if the popover is shown, <code>false</code>
* otherwise
*/
public boolean isOpen() {
try {
return getPropertyBoolean("opened");
} catch (StaleElementReferenceException e) {
// The element is no longer even attached to the DOM
// -> it's not open
return false;
}
}
}

0 comments on commit c073c7a

Please sign in to comment.