Skip to content
Merged
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
20 changes: 20 additions & 0 deletions CodenameOne/src/com/codename1/ui/validation/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,22 @@ protected Object getComponentValue(Component cmp) {
/// @deprecated this method was exposed by accident, constraint implicitly calls it and you don't need to
/// call it directly. It will be made protected in a future update to Codename One!
public void bindDataListener(Component cmp) {
// Re-validate on focus loss in every configuration so a user who
// leaves the field by tapping into another field gets the same
// highlight feedback as a user who hits the VKB 'next' / Enter key.
// The action-listener registration below only catches the VKB Enter
// path; without an explicit focus-lost validate(), the field stays
// un-highlighted after a tap-away. See #1459.
cmp.addFocusListener(new FocusListener() {
@Override
public void focusGained(Component focused) {
}

@Override
public void focusLost(Component focused) {
validate(focused);
}
});
if (showErrorMessageForFocusedComponent) {
if (!(cmp instanceof InputComponent && ((InputComponent) cmp).isOnTopMode())) {
cmp.addFocusListener(new FocusListener() {
Expand Down Expand Up @@ -582,6 +598,10 @@ public void scrollChanged(int scrollX, int scrollY, int oldscrollX, int oldscrol

@Override
public void focusLost(Component cmp) {
// Validation on focus-loss is handled by the
// unconditional focus listener registered at the top
// of bindDataListener (see #1459); this listener is
// only responsible for the focus-gained error popup.
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.codename1.ui.validation;

import com.codename1.junit.FormTest;
import com.codename1.junit.UITestBase;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.TextField;
import com.codename1.ui.layouts.BoxLayout;

import static org.junit.jupiter.api.Assertions.*;

/**
* Regression test for
* https://github.com/codenameone/CodenameOne/issues/1459
*
* The 2015 reporter said that when a user typed invalid content into a
* validator-bound TextField and then moved focus by tapping into another
* field (rather than the VKB 'next' / Enter button), the previous field was
* not highlighted as invalid until the user returned to it. The
* action-listener path the Validator wires up only fires on VKB Enter /
* action; tapping a different field never went through that path.
*
* The fix registers an unconditional focus listener that re-validates on
* focus loss, so the highlight gets applied as soon as focus moves anywhere
* else.
*/
class ValidatorFocusLossHighlightTest extends UITestBase {

/// A constraint that simply requires non-empty text.
private static class NonEmptyConstraint implements Constraint {
@Override
public boolean isValid(Object value) {
return value != null && value.toString().length() > 0;
}

@Override
public String getDefaultFailMessage() {
return "must not be empty";
}
}

@FormTest
void focusLossOnAnotherFieldFlagsTheLeftFieldAsInvalid() {
Form f = Display.getInstance().getCurrent();
f.setLayout(BoxLayout.y());
TextField first = new TextField("good", "first");
TextField second = new TextField("", "second");
f.add(first).add(second);
f.revalidate();

Validator v = new Validator();
v.addConstraint(first, new NonEmptyConstraint());

// Initially the field is valid (text is "good").
assertTrue(v.isValid(first));

// Simulate the failure case: clear the field to an invalid value.
first.setText("");
first.requestFocus();
assertTrue(first.hasFocus());

// User taps into the second field. Before the fix the validator's
// focusLost handler was empty and the action-listener path only
// fired on VKB Enter, so validate(first) was never called and
// first stayed marked valid until it regained focus.
second.requestFocus();
assertFalse(first.hasFocus());

assertFalse(v.isValid(first),
"First field should now be marked invalid because focus left "
+ "it without correcting the value. See #1459.");
}

@FormTest
void focusLossDoesNotFalselyInvalidateAValidField() {
Form f = Display.getInstance().getCurrent();
f.setLayout(BoxLayout.y());
TextField first = new TextField("", "first");
TextField second = new TextField("", "second");
f.add(first).add(second);
f.revalidate();

Validator v = new Validator();
v.addConstraint(first, new NonEmptyConstraint());

first.setText("ok");
first.requestFocus();
second.requestFocus();

assertTrue(v.isValid(first),
"A field with valid content must stay valid after focus loss.");
}
}
Loading