Open Bug 1716336 Opened 3 years ago Updated 1 year ago

Kendo UI for jQuery DateTimePicker causes Firefox to jump to top of page when selecting values

Categories

(Web Compatibility :: Site Reports, defect)

Firefox 89
defect

Tracking

(firefox103 affected, firefox111 affected)

ASSIGNED
Tracking Status
firefox103 --- affected
firefox111 --- affected

People

(Reporter: mattw, Assigned: denschub)

Details

(Keywords: webcompat:site-wait)

User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36

Steps to reproduce:

https://demos.telerik.com/kendo-ui/datetimepicker/index

scroll down a little

it seems to be random, but so far, this has happened each time:

select today's date

select tomorrow's date

select a different time

select today's date

Actual results:

after selecting the date, the browser window jumps to the top of the page. This is affecting Date/Time Pickers in other applications where the D/T pickers are lower in the form.

Expected results:

the input fields should be populated and the scroll position should not change

The Bugbug bot thinks this bug should belong to the 'Core::Panning and Zooming' component, and is moving the bug to that component. Please revert this change in case you think the bot is wrong.

Component: Untriaged → Panning and Zooming
Product: Firefox → Core

Reproduces as far back as 2013-01-01 for me, I don't think we had apz then, so changing component. The demo is broken in 2012-01-01 so can't test much earlier.

Status: UNCONFIRMED → NEW
Component: Panning and Zooming → Layout: Scrolling and Overflow
Ever confirmed: true

The scrolling to the top is happening because JS code running on the page sets scrollTop.

Here is the JS backtrace at the point where scrollTop is set:

0 b(e = "[object HTMLLIElement]") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":82:21098]
    this = [object Window]
1 scroll(e = "[object HTMLLIElement]") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":83:2921]
    this = [object Object]
2 current(n = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":82:25769]
    this = [object Object]
3 select(t = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":83:3207]
    this = [object Object]
4 value(e = "Mon Jun 14 2021 01:00:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":83:3718]
    this = [object Object]
5 _update(t = "Mon Jun 14 2021 01:00:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":83:19050]
    this = [object Object]
6 _change(e = "Mon Jun 14 2021 01:00:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":83:17860]
    this = [object Object]
7 change("[object Object]") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":83:20568]
    this = [object Object]
8 trigger(e = ""change"") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":25:8037]
    this = [object Object]
9 navigateDown(e = "Mon Jun 14 2021 01:00:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":43:14179]
    this = [object Object]
10 _click(e = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":43:25123]
    this = [object Object]
11 init/o<(t = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js":43:11537]
    this = [object HTMLTableCellElement]
12 dispatch(a = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.511/js/jquery.min.js":3:12444]
    this = [object HTMLDivElement]
13 add/r.handle(a = "[object MouseEvent]") ["https://kendo.cdn.telerik.com/2021.2.511/js/jquery.min.js":3:9173]
    this = [object HTMLDivElement]

So, this is an issue for the page / framework author to investigate (i.e. why their code is setting scrollTop in response to this mouse event).

The described JavaScript callback is executed when the list with hours is displayed to scroll it to a certain time. It is not executed when the date in the calendar is selected. Thus I do not think it is responsible for the page jumping back to the top.

I got the stack trace by putting a breakpoint in the browser's code where it does the scrolling to the top. The browser's native stack trace tells me it's coming from the scrollTop setter, and the JS stack trace shows what code is setting the scrollTop.

I double-checked it again and got another stack trace (below); while the library version has changed to 2021.2.616 and the line numbers have changed, based on the function names it appears to be coming from the same place.

If it helps, the value that the JS code is trying to set scrollTop to is -443 (which then gets clamped to 0 because -443 is not a valid scroll offset).

I also verified that the element whose scrollTop property is being set is the page's root (<html>) element, so we're definitely looking at the right scroll action (i.e. the page being scrolled to the top).

0 b(e = "[object HTMLLIElement]") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":82:24574]
    this = [object Window]
1 scroll(e = "[object HTMLLIElement]") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":83:6460]
    this = [object Object]
2 current(n = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":82:29245]
    this = [object Object]
3 select(t = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":83:6746]
    this = [object Object]
4 value(e = "Mon Jun 21 2021 00:30:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":83:7257]
    this = [object Object]
5 _update(t = "Mon Jun 21 2021 00:30:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":83:22589]
    this = [object Object]
6 _change(e = "Mon Jun 21 2021 00:30:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":83:21399]
    this = [object Object]
7 change("[object Object]") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":83:24107]
    this = [object Object]
8 trigger(e = ""change"") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":25:8037]
    this = [object Object]
9 navigateDown(e = "Mon Jun 21 2021 00:30:00 GMT-0400 (Eastern Daylight Time)") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":43:14670]
    this = [object Object]
10 _click(e = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":43:25640]
    this = [object Object]
11 init/o<(t = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":43:12051]
    this = [object HTMLTableCellElement]
12 dispatch(a = "[object Object]") ["https://kendo.cdn.telerik.com/2021.2.616/js/jquery.min.js":3:12444]
    this = [object HTMLDivElement]
13 add/r.handle(a = "[object MouseEvent]") ["https://kendo.cdn.telerik.com/2021.2.616/js/jquery.min.js":3:9173]
    this = [object HTMLDivElement]

Perhaps if someone is able to provide a testcase that uses non-minified JS code, it would be easier to figure out what's going on.

Hi Botond!
Could I ask you to assign a priority for this bug since hiro is a little busy at the moment. Thanks!

Flags: needinfo?(botond)

I think signs so far are pointing towards this being a site issue rather than a browser issue, so I'm going to move this to the Web Compat component.

Component: Layout: Scrolling and Overflow → Desktop
Flags: needinfo?(botond)
Product: Core → Web Compatibility

I can't reproduce on macOS with Firefox Nightly 95.0a1 (2021-10-13) (64-bit) if the page is already at the top.
obviously I have forgotten to scroll

Shorter steps to reproduce.

  1. go to https://demos.telerik.com/kendo-ui/datetimepicker/index
  2. scroll a bit so there's room to jump to the top.
  3. Change the time
  4. Select another date

As soon as we scroll, the console displays the message:

This site appears to use a scroll-linked positioning effect. This may not work well with asynchronous panning; see https://firefox-source-docs.mozilla.org/performance/scroll-linked_effects.html for further details and to join the discussion on related tools and features!

This is a profile of what is happening.
https://share.firefox.dev/3vbiMIM

                    change: function () {
                        var value = that._applyDateValue();
                        if (options.singlePopup) {
                            if (!that.timeView._currentlySelected) {
                                that.timeView._currentlySelected = new Date();
                            }
                            that.timeView._currentlySelected.setFullYear(value.getFullYear());
                            that.timeView._currentlySelected.setMonth(value.getMonth());
                            that.timeView._currentlySelected.setDate(value.getDate());
                            that._switchToTimeView();
                            that._toggleIcons();
                        } else {
                            that._change(value);
                            that.close('date');
                        }
                    },

the time selected is, I don't know yet if it has an influence.

<li tabindex="-1" role="option" class="k-item k-state-selected" unselectable="on" id="datetimepicker_option_selected" aria-selected="true">1:30 AM</li>

and the ul is defined with

<ul tabindex="-1" role="listbox" aria-hidden="true" unselectable="on" class="k-list k-reset" 
       style="overflow: auto;" 
       id="datetimepicker_timeview">

it eventually reaches

            scroll: function (item) {
                if (!item) {
                    return;
                }
                if (item.scrollIntoViewIfNeeded) {
                    item.scrollIntoViewIfNeeded();
                } else {
                    scrollIntoViewIfNeeded(item);
                }
            },

which has this function

        function scrollIntoViewIfNeeded(element, centerIfNeeded) {…}

which contains.

        function scrollIntoViewIfNeeded(element, centerIfNeeded) {
            function makeRange(start, length) {
                return {
                    start: start,
                    length: length,
                    end: start + length
                };
            }
            function coverRange(inner, outer) {
                if (false === centerIfNeeded || outer.start < inner.end && inner.start < outer.end) {
                    return Math.min(inner.start, Math.max(outer.start, inner.end - outer.length));
                }
                return (inner.start + inner.end - outer.length) / 2;
            }
            function makePoint(x, y) {
                return {
                    x: x,
                    y: y,
                    translate: function translate(dX, dY) {
                        return makePoint(x + dX, y + dY);
                    }
                };
            }
            function absolute(elem, pt) {
                while (elem) {
                    pt = pt.translate(elem.offsetLeft, elem.offsetTop);
                    elem = elem.offsetParent;
                }
                return pt;
            }
            var target = absolute(element, makePoint(0, 0)), extent = makePoint(element.offsetWidth, element.offsetHeight), elem = element.parentNode, origin;
            while (elem instanceof HTMLElement) {
                origin = absolute(elem, makePoint(elem.clientLeft, elem.clientTop));
                elem.scrollLeft = coverRange(makeRange(target.x - origin.x, extent.x), makeRange(elem.scrollLeft, elem.clientWidth));
                elem.scrollTop = coverRange(makeRange(target.y - origin.y, extent.y), makeRange(elem.scrollTop, elem.clientHeight));
                target = target.translate(-elem.scrollLeft, -elem.scrollTop);
                elem = elem.parentNode;
            }
        }

And this is happening when after going through all the elements it reaches the HTML element.

I set logs on elem.scrollTop = coverRange(makeRange(target.y - origin.y, extent.y), makeRange(elem.scrollTop, elem.clientHeight));
with log: 'element Parent: ' + elem.nodeName + ' clientHeight: ' + elem.clientHeight + ' scrollTop: ' + elem.scrollTop
and a breakpoint on the subsequent line.

first test

changing date without changing time after the scroll.

No scroll is happening, but it doesn't go through the function scrollIntoViewIfNeeded at all.

second test

Now let's change the time.

10:33:26.293 element Parent: UL clientHeight: 200 scrollTop: 0 kendo.all.js:105905:96
10:33:26.297 element Parent: DIV clientHeight: 200 scrollTop: 0 kendo.all.js:105905:96
10:33:26.300 element Parent: DIV clientHeight: 202 scrollTop: 0 kendo.all.js:105905:96
10:33:26.303 element Parent: BODY clientHeight: 2040 scrollTop: 0 kendo.all.js:105905:96
10:33:26.305 element Parent: HTML clientHeight: 1136 scrollTop: 318 kendo.all.js:105905:96
10:33:26.308 element Parent: UL clientHeight: 200 scrollTop: 0 kendo.all.js:105905:96
10:33:26.309 element Parent: DIV clientHeight: 200 scrollTop: 0 kendo.all.js:105905:96
10:33:26.310 element Parent: DIV clientHeight: 202 scrollTop: 0 kendo.all.js:105905:96
10:33:26.311 element Parent: BODY clientHeight: 2040 scrollTop: 0 kendo.all.js:105905:96
10:33:26.313 element Parent: HTML clientHeight: 1136 scrollTop: 318 kendo.all.js:105905:96

BUT NO SCROLL

third test

Let's change the date (after the time)

10:34:36.656 element Parent: UL clientHeight: 0 scrollTop: 0 kendo.all.js:105905:96
10:35:24.353 element Parent: DIV clientHeight: 0 scrollTop: 0 2 kendo.all.js:105905:96
10:35:32.186 element Parent: BODY clientHeight: 2040 scrollTop: 0 kendo.all.js:105905:96
10:35:39.964 element Parent: HTML clientHeight: 1136 scrollTop: 318 kendo.all.js:105905:96
10:35:49.171 element Parent: UL clientHeight: 0 scrollTop: 0 kendo.all.js:105905:96
10:35:54.803 element Parent: DIV clientHeight: 0 scrollTop: 0 2 kendo.all.js:105905:96
10:35:59.918 element Parent: BODY clientHeight: 2040 scrollTop: 0 kendo.all.js:105905:96
10:36:07.691 element Parent: HTML clientHeight: 1136 scrollTop: 0 kendo.all.js:105905:96

SCROLL on this last line. The difference with the previous sequence is that this time suddenly the scrollTop value has been set to 0.

centerIfNeeded is never defined in the code, so it doesn't have an influence

In Safari it never goes through the scrollIntoViewIfNeeded
In Blink it also never goes through it.

            scroll: function (item) {
                if (!item) {
                    return;
                }
                if (item.scrollIntoViewIfNeeded) {
                    item.scrollIntoViewIfNeeded();
                } else {
                    scrollIntoViewIfNeeded(item);
                }
            },

item seems undefined in both Blink and Safari. So this is happening a lot before. They are just not reaching this code path.

Karl, out of curiosity, how were you able to get the non-minified code quoted in comment 9?

Botond,

(just in case)

  1. in ••• top right of the devtools window, select settings
  2. Search for advanced settings
  3. Enable Source Maps
  4. Close devtools, reopen.

Then in the debugger if you go to
https://kendo.cdn.telerik.com/2021.3.914/js/kendo.all.js

The {} is not visible.
so here there's a trick.

On the file name in the tab, You can right+click or ctrl+click, there is a contextual menu with "Pretty Print source".

It will beautify the code.

Thanks. I was familiar with pretty-printing, but I didn't realize the framework had the non-minified source available in the debugger.

One more thing I'm curious about: I often get JS backtraces by putting a breakpoint in native code and running DumpJSStack(). Is there a way for me to map a location from that stack trace (e.g. "https://kendo.cdn.telerik.com/2021.2.616/js/kendo.all.min.js":82:24574) to a location in the corresponding non-minified file?

This is still an issue.

Tested with:
Browser / Version: Firefox Nightly 103.0a1 (2022-06-26)
Operating System: Windows 10 Pro

The issue is still reproducible on the latest Nightly.

Tested on:
Operating system: Windows 10
Browser/Version: Nightly 111.0a1 (2023-01-18) / Chrome 109.0.5414.75

Note: Not reproducible on Chrome.

Assignee: nobody → dschubert
Status: NEW → ASSIGNED
You need to log in before you can comment on or make changes to this bug.