Skip to content

Responsive Stepper: Window resizing behavior is not consistent #1039

@bschmelcher

Description

@bschmelcher

Describe the bug
We use a stepper component which should responsively change from horizontal to vertical for small screen sizes.
The horizontal stepper should show inlined labels using PrefixAddOns for the StepTracker.
The vertical stepper should show normal labels (ParagraphElements) left of the vertical stepper using dui_reversed.

As suggested I added both the PrefixAddons and the ParagraphElements to the StepperTrack for each step and use handlers to hide them.

I tried two combinations to achieve this:
1)
Use the dui-responsive css-class for the stepper - together with hideOn handlers for the PrefixAddOns and ParagraphElements
2)
Without the dui-responsive css-class: Use MediaQuery listeners to add/remove the dui_vertical css class to the stepper - together with MediaQueryListeners for the PrefixAddOns and ParagraphElements to hide/show them

In the appended code, both combinations can be configured using the SMALL_AND_UP_DOWN_WITH_DUI_RESPONSIVE for 1) or MEDIUM_AND_UP_DOWN_WITH_MEDIAQUERIES for 2) flags.

For case 1) this is the behavior:
coming from a wide window:
>992px: Horizontal stepper is shown correctly with Inline-Labels
<992px: Correct change from horizontal to vertical, Labels disappear completely
<768px: Non-inline labels appear, but stepper looks off and moves when further decreased until 754px
<754px: Stepper doesn't move anymore, but still looks a bit off

coming from a small window:
<754px: Stepper doesn't move, but looks off
>754px: Stepper moves with every pixel increase, Non-inline Labels are present
>768px: Labels disappear
>992px: Correct change from vertical to horizontal, Inline-Labels are present again

It looks strange to me that the labels disappear even though I'm using the SMALL_AND_DOWN and SMALL_AND_UP ScreenMedia classes, which I would have expected to work together with the dui-responsive css class (small-and-down).
Also the stepper should not move between 754px and 768px,
Below 754px the stepper should be a straight line instead of looking like stairs

For case 2) this is the behavior:
coming from a wide window and lowering width:
>1200px: Horizontal stepper is shown correctly
<=1200px: Correct change from horizontal to vertical
If now the window width is only decreased to a value >990px and then increased again, the stepper will never be horizontal again until width was under 990px

coming from a small window and increasing width:
<992px: Vertical stepper is shown correctly
>=992px: Correct change from vertical to horizontal
If now the window width is only increased to a value <1200px and then decreased again, the stepper will never be vertical again until width was over 1200px

Here the change from horizontal to vertical and vice versa is correct, but it is necessary to reach certain breakpoints after the width change so the other way around works as well.
If these breakpoints are not met, the stepper will stay in the same orientation regardless of how wide/small the screen is.
E.g. if the window is on 1300px (horizontal), resized to 1100px (vertical) and then increased to 2000px (still vertical)
I would expect that the change happens at one single breakpoint (1200px), instead of first having to reduce the size to 990px first.

One reason seems to be the difference in pixels for the min- and max-width of the domino-ui-screen--down.css and domino-ui-screen--up.css files

To Reproduce
Steps to reproduce the behavior:

  1. See attached demo code
  2. For case 1, set SMALL_AND_UP_DOWN_WITH_DUI_RESPONSIVE = true and MEDIUM_AND_UP_DOWN_WITH_MEDIAQUERIES = false
    For case 2, set SMALL_AND_UP_DOWN_WITH_DUI_RESPONSIVE = false and MEDIUM_AND_UP_DOWN_WITH_MEDIAQUERIES = true
  3. Run the application
  4. Resize the window as described above (or in the javadoc above the boolean flags)

Expected behavior
Case 1) The stepper labels should not disappear, the vertical stepper should not look like stairs and should not move during reducing window width
Case 2) The direction should change at a certain breakpoint, without having to reach lower or higher breakpoints

Screenshots

Image
Image

Additional context

Code example
import com.google.gwt.core.client.EntryPoint;
import elemental2.dom.DomGlobal;
import elemental2.dom.HTMLElement;
import org.dominokit.domino.ui.button.Button;
import org.dominokit.domino.ui.cards.Card;
import org.dominokit.domino.ui.elements.ParagraphElement;
import org.dominokit.domino.ui.forms.*;
import org.dominokit.domino.ui.forms.validations.ValidationResult;
import org.dominokit.domino.ui.icons.lib.Icons;
import org.dominokit.domino.ui.layout.AppLayout;
import org.dominokit.domino.ui.mediaquery.MediaQuery;
import org.dominokit.domino.ui.stepper.Step;
import org.dominokit.domino.ui.stepper.StepState;
import org.dominokit.domino.ui.stepper.StepTracker;
import org.dominokit.domino.ui.stepper.Stepper;
import org.dominokit.domino.ui.utils.PostfixAddOn;
import org.dominokit.domino.ui.utils.PrefixAddOn;
import org.dominokit.domino.ui.utils.ScreenMedia;
import java.util.Objects;
import static org.dominokit.domino.ui.style.DisplayCss.dui_flex;
import static org.dominokit.domino.ui.style.GenericCss.dui_success;
import static org.dominokit.domino.ui.style.SpacingCss.*;
import static org.dominokit.domino.ui.utils.Domino.*;

public class StepperIssue implements EntryPoint {

    @Override
    public void onModuleLoad() {
        AppLayout layout = AppLayout.create().show();
        DomGlobal.document.body.appendChild(layout.element());
        Card card = Card.create("Stepper");
        Stepper stepper = createStepper();
        card.appendChild(stepper);
        layout.getContent().appendChild(card);
        stepper.start(StepState.ACTIVE);
    }

    boolean useInlineLabels = true;
    boolean useAboveLabels = true;

    /**
     * true: use dui_responsive for stepper, hideOn methods for labels
     * false: disabled
     * <p>
     * coming from a wide window:
     * <p>
     * >992px: Horizontal stepper is shown correctly with Labels
     * <992px: Correct change from horizontal to vertical, Labels disappear completely
     * <768px: Non-inline labels appear, but stepper looks off and moves when further decreased until 754px
     * <754px: Stepper doesn't move anymore, but still looks a bit off
     * <p>
     * coming from a small window:
     * <754px: Stepper doesn't move, but looks off
     * >754px: Stepper moves with every pixel increase, Non-inline Labels are present
     * >768px: Labels disappear
     * >992px: Correct change from vertical to horizontal, Labels are present again
     */
    boolean SMALL_AND_UP_DOWN_WITH_DUI_RESPONSIVE = true;

    /**
     * true: use MediaQueries to set/remove dui_vertical of the stepper, MediaQueries also for labels
     * false: disabled
     * <p>
     * coming from a wide window and lowering width:
     * >1200px: Horizontal stepper is shown correctly
     * <=1200px: Correct change from horizontal to vertical
     * If now the window width is only decreased to a value >990px and then increased again, the stepper will never be horizontal again until width was under 990px
     * <p>
     * coming from a small window and increasing width:
     * <992px: Vertical stepper is shown correctly
     * >=992px: Correct change from vertical to horizontal
     * If now the window width is only increased to a value <1200px and then decreased again, the stepper will never be vertical again until width was over 1200px
     */
    boolean MEDIUM_AND_UP_DOWN_WITH_MEDIAQUERIES = false;

    private void createTrackerWithLabels(StepTracker tracker, String title) {
        PrefixAddOn<HTMLElement> inlineLabel = PrefixAddOn.of(span().textContent(title).addCss(dui_p_x_4));
        ParagraphElement label = p(title).addCss(dui_m_0, dui_p_l_2).setTextAlign("end");
        if (SMALL_AND_UP_DOWN_WITH_DUI_RESPONSIVE) {
            inlineLabel.hideOn(ScreenMedia.SMALL_AND_DOWN);
            label.hideOn(ScreenMedia.SMALL_AND_UP);
        }
        if (MEDIUM_AND_UP_DOWN_WITH_MEDIAQUERIES) {
            MediaQuery.addOnMediumAndDownListener(() -> {
                inlineLabel.hide();
                label.show();
            });
            MediaQuery.addOnMediumAndUpListener(() -> {
                inlineLabel.show();
                label.hide();
            });
        }
        if (useInlineLabels) {
            tracker.appendChild(inlineLabel);
        }

        if (useAboveLabels) {
            tracker.appendChild(label);
        }
    }

    private Stepper createStepper() {
        Stepper stepper = Stepper.create();

        if (SMALL_AND_UP_DOWN_WITH_DUI_RESPONSIVE) {
            stepper.addCss(dui_responsive);
        }
        if (MEDIUM_AND_UP_DOWN_WITH_MEDIAQUERIES) {
            MediaQuery.addOnMediumAndUpListener(() -> stepper.removeCss(dui_vertical));
            MediaQuery.addOnMediumAndDownListener(() -> stepper.addCss(dui_vertical));
        }

        stepper.setHideStepperTail(true)
                .addCss(
                        dui_reversed)
                .appendChild(Step.create()
                        .withTracker((parent, tracker) -> {
                            createTrackerWithLabels(tracker, "1. Personal info");
                        })
                        .withHeader((parent, header) -> {
                            header
                                    .appendChild(PrefixAddOn.of(Icons.account()))
                                    .appendChild(PostfixAddOn.of(Icons.dots_vertical()))
                                    .setDescription("Name and nickname will be used in notifications and user info.")
                            ;
                        })
                        .withContent((step, content) -> {
                            FieldsGrouping stepGroup = FieldsGrouping.create();
                            TextBox firstName = TextBox.create("First name")
                                    .groupBy(stepGroup);
                            TextBox lastName = TextBox.create("Last name")
                                    .groupBy(stepGroup);
                            TextBox nickname = TextBox.create("Nickname")
                                    .groupBy(stepGroup);
                            stepGroup.setRequired(true);

                            content
                                    .appendChild(div()
                                            .addCss(dui_flex, dui_p_x_48, dui_items_center, dui_justify_center, dui_flex_col)
                                            .appendChild(firstName)
                                            .appendChild(lastName)
                                            .appendChild(nickname)
                                    );

                            step.addEventListener("stepreset", evt -> {
                                stepGroup.clear(true);
                                step.setState(StepState.INACTIVE);
                            });

                            step.withFooter((parent, footer) -> {
                                footer
                                        .addCss(dui_flex, dui_justify_center, dui_gap_1, dui_p_4)
                                        .appendChild(Button.create("Next")
                                                .addCss(dui_success, dui_w_24)
                                                .setIcon(Icons.arrow_right())
                                                .setReversed(true)
                                                .addClickListener(evt -> {
                                                    if (stepGroup.validate().isValid()) {
                                                        step.next((deactivated, activated) -> {
                                                            deactivated.ifPresent(stepTracker -> step.setState(StepState.COMPLETED));
                                                            activated.ifPresent(stepTracker -> stepTracker.setState(StepState.ACTIVE));
                                                        });
                                                    } else {
                                                        step.setState(StepState.ERROR);
                                                    }
                                                })
                                        );
                            });
                        })
                )
                .appendChild(Step.create()
                        .withTracker((parent, tracker) -> {
                            createTrackerWithLabels(tracker, "2. Address");
                        })
                        .withHeader((parent, header) -> {
                            header
                                    .appendChild(PrefixAddOn.of(Icons.map_marker()))
                                    .appendChild(PostfixAddOn.of(Icons.dots_vertical()))
                                    .setDescription("Will be used for shipments.")
                            ;
                        })
                        .withContent((step, content) -> {
                            FieldsGrouping stepGroup = FieldsGrouping.create();
                            TextBox country = TextBox.create("Country")
                                    .groupBy(stepGroup);
                            TextBox city = TextBox.create("City")
                                    .groupBy(stepGroup);
                            TextBox zipCode = TextBox.create("ZIP Code")
                                    .setHelperText("Leave empty to mark step with warning.");
                            stepGroup.setRequired(true);
                            step.addEventListener("stepreset", evt -> {
                                stepGroup.clear(true);
                                zipCode.clear(true);
                                step.setState(StepState.INACTIVE);
                            });
                            content
                                    .appendChild(div()
                                            .addCss(dui_flex, dui_p_x_48, dui_items_center, dui_justify_center, dui_flex_col)
                                            .appendChild(country)
                                            .appendChild(city)
                                            .appendChild(zipCode)
                                    );

                            step.withFooter((parent, footer) -> {
                                footer
                                        .addCss(dui_justify_center, dui_gap_1, dui_p_4)
                                        .appendChild(Button.create("Back")
                                                .setIcon(Icons.arrow_left())
                                                .addClickListener(evt -> {
                                                    step.prev((deactivated, activated) -> {
                                                        deactivated.ifPresent(stepTracker -> step.setState(StepState.INACTIVE));
                                                        activated.ifPresent(stepTracker -> stepTracker.setState(StepState.ACTIVE));
                                                    });
                                                })
                                        )
                                        .appendChild(Button.create("Next")
                                                .addCss(dui_success)
                                                .setIcon(Icons.arrow_right())
                                                .setReversed(true)
                                                .addClickListener(evt -> {
                                                    if (stepGroup.validate().isValid()) {
                                                        if (zipCode.isEmpty()) {
                                                            step.next((deactivated, activated) -> {
                                                                deactivated.ifPresent(stepTracker -> step.setState(StepState.WARNING));
                                                                activated.ifPresent(stepTracker -> stepTracker.setState(StepState.ACTIVE));
                                                            });
                                                        } else {
                                                            step.next((deactivated, activated) -> {
                                                                deactivated.ifPresent(stepTracker -> step.setState(StepState.COMPLETED));
                                                                activated.ifPresent(stepTracker -> stepTracker.setState(StepState.ACTIVE));
                                                            });
                                                        }
                                                    } else {
                                                        step.setState(StepState.ERROR);
                                                    }
                                                })
                                        );
                            });
                        })
                )
                .appendChild(Step.create()
                        .withTracker((parent, tracker) -> {
                            createTrackerWithLabels(tracker, "3. Contact information");
                        })
                        .withHeader((parent, header) -> {
                            header
                                    .appendChild(PrefixAddOn.of(Icons.phone()))
                                    .appendChild(PostfixAddOn.of(Icons.dots_vertical()))
                                    .setDescription("Will be used for notifications and verification.")
                            ;
                        })
                        .withContent((step, content) -> {
                            FieldsGrouping stepGroup = FieldsGrouping.create();
                            EmailBox email = EmailBox.create("Email")
                                    .groupBy(stepGroup);
                            TelephoneBox phone = TelephoneBox.create("Phone number")
                                    .groupBy(stepGroup);
                            stepGroup.setRequired(true);
                            step.addEventListener("stepreset", evt -> {
                                stepGroup.clear(true);
                                step.setState(StepState.INACTIVE);
                            });
                            content
                                    .appendChild(div()
                                            .addCss(dui_flex, dui_p_x_48, dui_items_center, dui_justify_center, dui_flex_col)
                                            .appendChild(email)
                                            .appendChild(phone)
                                    );

                            step.withFooter((parent, footer) -> {
                                footer
                                        .addCss(dui_justify_center, dui_gap_1, dui_p_4)
                                        .appendChild(Button.create("Back")
                                                .setIcon(Icons.arrow_left())
                                                .addClickListener(evt -> {
                                                    step.prev((deactivated, activated) -> {
                                                        deactivated.ifPresent(stepTracker -> step.setState(StepState.INACTIVE));
                                                        activated.ifPresent(stepTracker -> stepTracker.setState(StepState.ACTIVE));
                                                    });
                                                })
                                        )
                                        .appendChild(Button.create("Next")
                                                .addCss(dui_success)
                                                .setIcon(Icons.arrow_right())
                                                .setReversed(true)
                                                .addClickListener(evt -> {
                                                    if (stepGroup.validate().isValid()) {
                                                        step.next((deactivated, activated) -> {
                                                            deactivated.ifPresent(stepTracker -> step.setState(StepState.COMPLETED));
                                                            activated.ifPresent(stepTracker -> stepTracker.setState(StepState.ACTIVE));
                                                        });
                                                    } else {
                                                        step.setState(StepState.ERROR);
                                                    }
                                                })
                                        );
                            });
                        })
                )
                .appendChild(Step.create()
                        .withTracker((parent, tracker) -> {
                            createTrackerWithLabels(tracker, "4. Security");
                        })
                        .withHeader((parent, header) -> {
                            header
                                    .appendChild(PrefixAddOn.of(Icons.shield()))
                                    .appendChild(PostfixAddOn.of(Icons.dots_vertical()))
                                    .setDescription("Will be used for login and verification.")
                            ;
                        })
                        .withContent((step, content) -> {
                            FieldsGrouping stepGroup = FieldsGrouping.create();
                            PasswordBox password = PasswordBox.create("Password")
                                    .groupBy(stepGroup);
                            PasswordBox confirm = PasswordBox.create("Confirm password")
                                    .groupBy(stepGroup);
                            step.addEventListener("stepreset", evt -> {
                                stepGroup.clear(true);
                                step.setState(StepState.INACTIVE);
                            });
                            stepGroup
                                    .setRequired(true)
                                    .addValidator(group -> {
                                        if (Objects.equals(password.getValue(), confirm.getValue())) {
                                            return ValidationResult.valid();
                                        } else {
                                            return ValidationResult.invalid("Password and confirmation does not match.");
                                        }
                                    });

                            content
                                    .appendChild(div()
                                            .addCss(dui_flex, dui_p_x_48, dui_items_center, dui_justify_center, dui_flex_col)
                                            .appendChild(password)
                                            .appendChild(confirm)
                                    );

                            step.withFooter((parent, footer) -> {
                                footer
                                        .addCss(dui_justify_center, dui_gap_1, dui_p_4)
                                        .appendChild(Button.create("Back")
                                                .setIcon(Icons.arrow_left())
                                                .addClickListener(evt -> {
                                                    step.prev((deactivated, activated) -> {
                                                        deactivated.ifPresent(stepTracker -> step.setState(StepState.INACTIVE));
                                                        activated.ifPresent(stepTracker -> stepTracker.setState(StepState.ACTIVE));
                                                    });
                                                })
                                        )
                                        .appendChild(Button.create("Finish")
                                                .addCss(dui_success)
                                                .setIcon(Icons.arrow_right())
                                                .setReversed(true)
                                                .addClickListener(evt -> {
                                                    if (stepGroup.validate().isValid()) {
                                                        step.finish(StepState.COMPLETED);
                                                    } else {
                                                        step.setState(StepState.ERROR);
                                                    }
                                                })
                                        );
                            });
                        })
                );
        return stepper;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions