vaadin / flow

Vaadin Flow is a Java framework binding Vaadin web components to Java. This is part of Vaadin 10+.
Apache License 2.0
619 stars 167 forks source link

Scroll position resets to initial position after UI is hot-swapped #20371

Open abdullahtellioglu opened 2 weeks ago

abdullahtellioglu commented 2 weeks ago

Description of the bug

When an application exceeds the viewport and has a scrollbar, any change recreates the whole UI after hot-swapping, and the scrollbar goes to initial position.

https://github.com/user-attachments/assets/cac625ea-57ca-4f34-a1f5-9286bcb15564

Expected behavior

The scroll position should remain after reloading. I proposed a solution in Copilot repository to restore selected element position but scrollbar initially goes top and scrolls back to the element. Scrollbar restoring should work regardless of Copilot state so we moved the issue here as it is suggested by Artur

Minimal reproducible example

Create a view that exceeds the viewport such as :


@PageTitle("Kitchen Sink")
@Route(value = "sink", layout = MainLayout.class)
@AnonymousAllowed
public class KitchenSink extends VerticalLayout {
    public KitchenSink() {
        Div imStyled = new Div("I'm styled!");
        add(imStyled);
        Header header = new Header();
        header.setText("test");
        add(header);
        Accordion accordion = new Accordion();
        AccordionPanel accordionpanel = new AccordionPanel();
        Div accordionContent = new Div("accordion content");
        accordionpanel.add(accordionContent);
        accordion.add(accordionpanel);
        add(accordion);
        AvatarGroup avatargroup = new AvatarGroup();
        avatargroup.setMaxItemsVisible(2);
        AvatarGroupItem avatargroupitem = new AvatarGroupItem("Foo Bar");
        avatargroupitem.setColorIndex(1);
        AvatarGroupItem avatargroupitem2 = new AvatarGroupItem();
        avatargroupitem2.setColorIndex(2);
        AvatarGroupItem avatargroupitem3 = new AvatarGroupItem("Foo Bar");
        avatargroupitem3.setColorIndex(3);
        avatargroup.setItems(avatargroupitem, avatargroupitem2, avatargroupitem3);
        add(avatargroup);
        Avatar avatar = new Avatar("Jens Jansson");
        avatar.setAbbreviation("SK");
        add(avatar);
        Board board = new Board();
        Row row = new Row();
        Paragraph board2 = new Paragraph("Board");
        row.add(board2);
        board.add(row);
        Row row2 = new Row();
        Div topAA = new Div("top aA");
        topAA.setClassName("top a");
        row2.add(topAA);
        Div topB = new Div("top B");
        topB.setClassName("top b");
        row2.add(topB);
        Div topC = new Div("top C");
        topC.setClassName("top c");
        row2.add(topC);
        board.add(row2);
        Row row3 = new Row();
        Div mid = new Div("mid");
        mid.setClassName("mid");
        row3.add(mid);
        board.add(row3);
        Row row4 = new Row();
        Div lowA = new Div("low A");
        lowA.setClassName("low a");
        row4.add(lowA);
        Row row5 = new Row();
        Div lowBA = new Div("low B / A");
        lowBA.setClassName("top a");
        row5.add(lowBA);
        Div lowBB = new Div("low B / B");
        lowBB.setClassName("top b");
        row5.add(lowBB);
        Div lowBC = new Div("low B / C");
        lowBC.setClassName("top c");
        row5.add(lowBC);
        Div lowBD = new Div("low B / D");
        lowBD.setClassName("top d");
        row5.add(lowBD);
        row4.add(row5);
        board.add(row4);
        add(board);
        Button primary = new Button("Primary");
        primary.setThemeName("primary");
        primary.setId("confirm");
        add(primary);
        Button secondary = new Button("Secondary");
        secondary.setThemeName("secondary");
        add(secondary);
        Button tertiary = new Button("Tertiary");
        tertiary.setThemeName("tertiary");
        add(tertiary);
        Icon icon = new Icon("vaadin:user");
        add(icon);
        Icon icon2 = new Icon("foo:bar");
        add(icon2);
        CheckboxGroup checkboxgroup = new CheckboxGroup("Label");
        checkboxgroup.setThemeName("vertical");
        Checkbox checkbox = new Checkbox("Option one");
        checkboxgroup.add(checkbox);
        Checkbox checkbox2 = new Checkbox("Option two");
        checkboxgroup.add(checkbox2);
        Checkbox checkbox3 = new Checkbox("Option three");
        checkboxgroup.add(checkbox3);
        add(checkboxgroup);

        Grid<SamplePerson> grid = new Grid<>(SamplePerson.class);
        add(grid);

        CheckboxGroup checkboxGroupWithDataProvider = new CheckboxGroup("CB group with data provider");
        checkboxGroupWithDataProvider.setItems(List.of("Item 1", "Item 2", "Item 3"));
        add(checkboxGroupWithDataProvider);

        ComboBox combobox = new ComboBox();
        combobox.setItems(1, 2, 3, 4, 5);
        add(combobox);
        MultiSelectComboBox multiselectcombobox = new MultiSelectComboBox();
        multiselectcombobox.setItems("apple", "banana", "lemon", "orange");
        add(multiselectcombobox);
        ConfirmDialog confirmdialog = new ConfirmDialog();
        add(confirmdialog);
        CookieConsent cookieconsent = new CookieConsent();
        add(cookieconsent);
        DatePicker datepicker = new DatePicker();
        add(datepicker);
        DateTimePicker datetimepicker = new DateTimePicker();
        add(datetimepicker);
        Details details = new Details();
        Div details2 = new Div("Details");
        details.add(details2);
        add(details);
        Dialog dialog = new Dialog();
        add(dialog);
        Select select = new Select();
        add(select);
        TimePicker timepicker = new TimePicker();
        add(timepicker);
        DrawerToggle drawertoggle = new DrawerToggle();
        add(drawertoggle);
        FormLayout formlayout = new FormLayout();
        TextField textfield = new TextField();
        formlayout.add(textfield);
        PasswordField passwordfield = new PasswordField();
        formlayout.add(passwordfield);
        EmailField emailfield = new EmailField();
        formlayout.add(emailfield);
        IntegerField integerfield = new IntegerField();
        formlayout.add(integerfield);
        NumberField numberfield = new NumberField();
        formlayout.add(numberfield);
        TextArea textarea = new TextArea();
        formlayout.add(textarea);
        add(formlayout);
        VerticalLayout verticallayout = new VerticalLayout();
        HorizontalLayout horizontallayout = new HorizontalLayout();
        H1 h1 = new H1("H1");
        horizontallayout.add(h1);
        H2 h2 = new H2("H2");
        horizontallayout.add(h2);
        H3 h3 = new H3("H3");
        horizontallayout.add(h3);
        H4 h4 = new H4("H4");
        horizontallayout.add(h4);
        H5 h5 = new H5("H5");
        horizontallayout.add(h5);
        H6 h6 = new H6("H6");
        horizontallayout.add(h6);
        verticallayout.add(horizontallayout);
        Header header2 = new Header();
        header2.setText("Header");
        Div dIV = new Div("DIV");
        Footer footer = new Footer();
        footer.setText("Footer");
        footer.setClassName("AA");
        add(verticallayout);
        LoginForm loginform = new LoginForm();
        add(loginform);
        Button openLoginOverlay = new Button("Open Login Overlay");
        add(openLoginOverlay);
        LoginOverlay loginoverlay = new LoginOverlay();
        add(loginoverlay);
        ProgressBar progressbar = new ProgressBar();
        progressbar.setIndeterminate(true);
        add(progressbar);
        RichTextEditor richtexteditor = new RichTextEditor();
        add(richtexteditor);
        SplitLayout splitlayout = new SplitLayout();
        Div div = new Div();
        Button rIGHT = new Button("RIGHT");
        rIGHT.setClassName("AAA");
        richtexteditor.addClassName("Abdullah");
        richtexteditor.addClassName("Abdullah");
        div.add(rIGHT);
        splitlayout.addToSecondary(div);
        Div div2 = new Div();
        Button lEFT = new Button("LEFT");
        div2.add(lEFT);
        splitlayout.addToPrimary(div2);
        add(splitlayout);
        Upload upload = new Upload();
        add(upload);
        MessageInput messageinput = new MessageInput();
        add(messageinput);
        MessageList messagelist = new MessageList();
        add(messagelist);
    }

    public static class SamplePerson {
        private String firstName;
        private String lastName;
        private String email;
        private String phone;
        private LocalDate dateOfBirth;
        private String occupation;
        private String role;
        private boolean important;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }

        public String getPhone() {
            return phone;
        }

        public void setPhone(String phone) {
            this.phone = phone;
        }

        public LocalDate getDateOfBirth() {
            return dateOfBirth;
        }

        public void setDateOfBirth(LocalDate dateOfBirth) {
            this.dateOfBirth = dateOfBirth;
        }

        public String getOccupation() {
            return occupation;
        }

        public void setOccupation(String occupation) {
            this.occupation = occupation;
        }

        public String getRole() {
            return role;
        }

        public void setRole(String role) {
            this.role = role;
        }

        public boolean isImportant() {
            return important;
        }

        public void setImportant(boolean important) {
            this.important = important;
        }
    }
}

Update the code in IDEA and trigger hotswapping and you will see that scrollbar position resets the initial one.

Versions

Hilla: 24.6.0.alpha2 Flow: 24.6.0.alpha1 Vaadin: 24.6.0.alpha1 Copilot: 24.6-SNAPSHOT Frontend Hotswap: Enabled, using Vite OS: aarch64 Mac OS X 14.6.1 Java: JetBrains s.r.o. 17.0.10 Browser: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Java Hotswap: Hotswap is not enabled IDE Plugin: 1.3.3 IntelliJ

mshabarov commented 1 week ago

I've marked this as enhancement. The effort is rather big for precise scrolling: Flow has to know what provides the scrolling - page body or some component, then Flow has to calculate where to scroll based on actual changes on the page, e.g. what is removed/added. If too many components are removed, maybe it doesn't make sense to scroll at all.