Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

471 numberfield keyboard prompt #474

Merged
merged 4 commits into from
May 5, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,13 @@ public final class DemoComponentFactory implements Serializable {

private static final Comparator<Class<?>> CLASS_ORDER_COMPARATOR = Comparator.comparingInt(t -> t.isAnnotationPresent(Order.class) ? t.getAnnotation(Order.class).value() : Integer.MAX_VALUE - t.getSimpleName().charAt(0));

private static final ThreadLocal<DemoComponentFactory> COMPONENT_FACTORY = new ThreadLocal<>();

private static boolean isNotInterface(Class<?> type) {
return !type.isInterface();
}

public static DemoComponentFactory get() {
if(COMPONENT_FACTORY.get() == null) {
LOGGER.info("creating new instance of DemoComponentFactory");
COMPONENT_FACTORY.set(new DemoComponentFactory());
}
return COMPONENT_FACTORY.get();
// rolling back to creating a new instance every time, because of some threading issues
return new DemoComponentFactory();
}

private final Map<Class<? extends Component>, Component> components = new LinkedHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.vaadin.miki.demo.builders;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.html.Span;
import org.vaadin.miki.demo.ContentBuilder;
import org.vaadin.miki.demo.Order;
import org.vaadin.miki.markers.HasTextInputMode;
import org.vaadin.miki.shared.text.TextInputMode;

import java.util.function.Consumer;

/**
* Builds content for things that implement {@link HasTextInputMode}
* @author miki
* @since 2023-04-21
*/
@Order(119)
public class HasTextInputModeBuilder implements ContentBuilder<HasTextInputMode> {
@Override
public void buildContent(HasTextInputMode component, Consumer<Component[]> callback) {
final ComboBox<TextInputMode> modes = new ComboBox<>("Select text input mode:", TextInputMode.values());
modes.addValueChangeListener(event -> component.setTextInputMode(event.getValue()));
callback.accept(new Component[]{modes, new Span("Note: the change should affect the on-screen keyboard of your device.")});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.vaadin.miki.markers;

import org.vaadin.miki.shared.text.TextInputMode;

/**
* Marker interface for objects that have a text input mode.
*
* @author miki
* @since 2023-04-21
*/
public interface HasTextInputMode {

/**
* Changes the text input mode of this object.
* @param inputMode New input mode. Can be {@code null}.
*/
void setTextInputMode(TextInputMode inputMode);

/**
* Returns the current text input mode of this object.
* @return A {@link TextInputMode}, or {@code null} if none has been set.
*/
TextInputMode getTextInputMode();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.vaadin.miki.markers;

import org.vaadin.miki.shared.text.TextInputMode;

/**
* Mixin interface for {@link HasTextInputMode}.
*
* @author miki
* @since 2023-04-21
*/
@SuppressWarnings("squid:S119") // SELF is a good generic name
public interface WithTextInputModeMixin<SELF extends WithTextInputModeMixin<SELF>> extends HasTextInputMode {

/**
* Chains {@link #setTextInputMode(TextInputMode)} and returns itself.
* @param mode New text input mode.
* @return This.
*/
@SuppressWarnings("unchecked") // should be fine
default SELF withTextInputMode(TextInputMode mode) {
this.setTextInputMode(mode);
return (SELF) this;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.vaadin.miki.shared.text;

/**
* All values supported by {@code inputmode} attribute of an {@code input} html element.
*
* @author miki
* @since 2023-04-21
*/
public enum TextInputMode {

// values taken from https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode

NONE, TEXT, DECIMAL, NUMERIC, TEL, SEARCH, EMAIL, URL

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
import org.vaadin.miki.markers.WithPlaceholderMixin;
import org.vaadin.miki.markers.WithReceivingSelectionEventsFromClientMixin;
import org.vaadin.miki.markers.WithRequiredMixin;
import org.vaadin.miki.markers.WithTextInputModeMixin;
import org.vaadin.miki.markers.WithTooltipMixin;
import org.vaadin.miki.markers.WithValueMixin;
import org.vaadin.miki.shared.labels.LabelPosition;
import org.vaadin.miki.shared.text.TextInputMode;
import org.vaadin.miki.superfields.text.SuperTextField;

import java.text.DecimalFormat;
Expand All @@ -56,7 +58,6 @@
* @since 2020-04-07
*/
@CssImport("./styles/form-layout-number-field-styles.css")
//@CssImport(value = "./styles/label-positions.css", themeFor = "super-text-field")
@SuppressWarnings("squid:S119") // SELF is a perfectly fine generic name that indicates its purpose
public abstract class AbstractSuperNumberField<T extends Number, SELF extends AbstractSuperNumberField<T, SELF>>
extends CustomField<T>
Expand All @@ -66,7 +67,8 @@ public abstract class AbstractSuperNumberField<T extends Number, SELF extends Ab
WithValueMixin<AbstractField.ComponentValueChangeEvent<CustomField<T>, T>, T, SELF>,
WithIdMixin<SELF>, WithNullValueOptionallyAllowedMixin<SELF, AbstractField.ComponentValueChangeEvent<CustomField<T>, T>, T>,
WithHelperMixin<SELF>, WithHelperPositionableMixin<SELF>, WithClearButtonMixin<SELF>,
WithRequiredMixin<SELF>, WithLabelPositionableMixin<SELF>, WithTooltipMixin<SELF> {
WithRequiredMixin<SELF>, WithLabelPositionableMixin<SELF>, WithTooltipMixin<SELF>,
WithTextInputModeMixin<SELF> {

private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSuperNumberField.class);

Expand Down Expand Up @@ -164,6 +166,7 @@ protected AbstractSuperNumberField(T defaultValue, SerializablePredicate<T> nega
if(maxFractionDigits >= 0)
this.format.setMaximumFractionDigits(maxFractionDigits);
this.updateRegularExpression();
this.updateTextInputMode();

this.field.addClassName(TEXT_FIELD_STYLE_PREFIX +this.getClass().getSimpleName().toLowerCase());
this.add(this.field);
Expand All @@ -174,6 +177,7 @@ protected AbstractSuperNumberField(T defaultValue, SerializablePredicate<T> nega
this.field.addFocusListener(this::onFieldSelected);
this.field.addBlurListener(this::onFieldBlurred);
this.field.addTextSelectionListener(this::onTextSelected);

// the following line allows for ValueChangeMode to be effective (#337)
// at the same time, it makes setting fraction/integer digits destructive (see #339)
// (because without it the value would be updated on blur, and not on every change)
Expand Down Expand Up @@ -294,6 +298,7 @@ protected void setMinimumFractionDigits(int digits) {
protected void setMaximumFractionDigits(int digits) {
this.format.setMaximumFractionDigits(digits);
this.updateRegularExpression(true);
this.updateTextInputMode();
}

/**
Expand Down Expand Up @@ -331,6 +336,28 @@ protected final void updateRegularExpression(boolean ignoreValueChangeFromField)
else this.updateRegularExpression();
}

/**
* Specifies the allowed characters and prevents invalid input.
*
* @param builder Builder to be used. Note that the builder passed to it already starts with {@code [\d} and {@code ]} is added at the end.
* @return The passed builder with added allowed characters.
*/
// note that this still allows entering a sequence of valid characters that is invalid (does not match the pattern)
// see #473
protected StringBuilder buildAllowedCharPattern(StringBuilder builder) {
builder.append(this.format.getDecimalFormatSymbols().getGroupingSeparator());
// allow regular space if NBS is used
if(this.format.getDecimalFormatSymbols().getGroupingSeparator() == NON_BREAKING_SPACE)
builder.append(" ");
if(this.getMaximumFractionDigits() > 0)
builder.append(this.format.getDecimalFormatSymbols().getDecimalSeparator());
// this should be last character to avoid implicit ranges
if(this.isNegativeValueAllowed())
builder.append(this.format.getDecimalFormatSymbols().getMinusSign());

return builder;
}

/**
* Builds the regular expression for matching the input.
*/
Expand All @@ -342,6 +369,9 @@ protected final void updateRegularExpression() {

this.field.setPattern(this.regexp);

final String allowedChars = this.buildAllowedCharPattern(new StringBuilder("[\\d")).append("]").toString();
this.field.setAllowedCharPattern(allowedChars);

LOGGER.debug("pattern updated to {}", this.regexp);
if(!this.isNegativeValueAllowed() && value != null && this.negativityPredicate.test(value)) {
LOGGER.debug("negative values are not allowed, so turning into positive value {}", value);
Expand All @@ -351,6 +381,18 @@ protected final void updateRegularExpression() {
this.setPresentationValue(value);
}

/**
* Updates the underlying field's text input mode.
* This shows a proper on-screen keyboard on devices that support it.
*/
// fixes #471
protected void updateTextInputMode() {
// if there are no fraction digits allowed, use NUMERIC (as DECIMAL shows decimal characters)
this.field.setTextInputMode(
this.getMaximumFractionDigits() == 0 ? TextInputMode.NUMERIC : TextInputMode.DECIMAL
);
}

/**
* Builds regular expression that allows neat typing of the number already formatted.
* Overwrite with care.
Expand Down Expand Up @@ -882,6 +924,16 @@ public LabelPosition getLabelPosition() {
return this.field.getLabelPosition();
}

@Override
public void setTextInputMode(TextInputMode inputMode) {
this.field.setTextInputMode(inputMode);
}

@Override
public TextInputMode getTextInputMode() {
return this.field.getTextInputMode();
}

/**
* Explicitly invokes code that would otherwise be called when the component receives focus.
* For testing purposes only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ public void setDecimalFormat(DecimalFormat format) {
}
}

@Override
protected StringBuilder buildAllowedCharPattern(StringBuilder builder) {
if(this.isScientificNotationEnabled())
builder.append(String.valueOf(this.getExponentSeparator()).toLowerCase(this.getLocale()))
.append(String.valueOf(this.getExponentSeparator()).toUpperCase(this.getLocale()));
return super.buildAllowedCharPattern(builder);
}

@Override
protected StringBuilder buildRegularExpression(StringBuilder builder, DecimalFormat format) {
builder = super.buildRegularExpression(builder.append("("), format).append(")");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
import org.vaadin.miki.markers.WithPlaceholderMixin;
import org.vaadin.miki.markers.WithReceivingSelectionEventsFromClientMixin;
import org.vaadin.miki.markers.WithRequiredMixin;
import org.vaadin.miki.markers.WithTextInputModeMixin;
import org.vaadin.miki.markers.WithTooltipMixin;
import org.vaadin.miki.markers.WithValueMixin;
import org.vaadin.miki.shared.text.TextInputMode;
import org.vaadin.miki.shared.text.TextModificationDelegate;

import java.util.Objects;

/**
* An extension of {@link TextField} with some useful (hopefully) features.
* @author miki
Expand All @@ -40,9 +44,11 @@ public class SuperTextField extends TextField implements CanSelectText, TextSele
WithIdMixin<SuperTextField>, WithLabelMixin<SuperTextField>, WithPlaceholderMixin<SuperTextField>,
WithValueMixin<AbstractField.ComponentValueChangeEvent<TextField, String>, String, SuperTextField>,
WithHelperMixin<SuperTextField>, WithHelperPositionableMixin<SuperTextField>,
WithReceivingSelectionEventsFromClientMixin<SuperTextField>, WithClearButtonMixin<SuperTextField>, WithTooltipMixin<SuperTextField> {
WithReceivingSelectionEventsFromClientMixin<SuperTextField>, WithClearButtonMixin<SuperTextField>,
WithTooltipMixin<SuperTextField>, WithTextInputModeMixin<SuperTextField> {

private final TextModificationDelegate<SuperTextField> delegate = new TextModificationDelegate<>(this, this.getEventBus(), this::getValue);
private TextInputMode textInputMode;

public SuperTextField() {
super();
Expand Down Expand Up @@ -127,6 +133,24 @@ public void modifyText(String replacement, int from, int to) {
this.delegate.modifyText(replacement, from, to);
}

@Override
public void setTextInputMode(TextInputMode inputMode) {
// only when there is a change
if(!Objects.equals(inputMode, this.getTextInputMode()))
this.getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse(this, context -> {
// js courtesy of the one and only JC, thank you!
if(inputMode != null)
this.getElement().executeJs("this.inputElement.inputMode = $0;", inputMode.name().toLowerCase());
else this.getElement().executeJs("delete this.inputElement.inputMode;" );
this.textInputMode = inputMode;
}));
}

@Override
public TextInputMode getTextInputMode() {
return this.textInputMode;
}

@SuppressWarnings("squid:S1185") // removing this method makes the class impossible to compile due to missing methods
@Override
public void setClearButtonVisible(boolean clearButtonVisible) {
Expand Down