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

Datepicker: Hide the UI on destroy #2268

Merged
merged 5 commits into from
Aug 5, 2024
Merged

Datepicker: Hide the UI on destroy #2268

merged 5 commits into from
Aug 5, 2024

Conversation

porterclev
Copy link
Contributor

Issue: #2178 - date picker remains after being destroyed

We confirmed through git bisect that this commit, 817ce38, introduced a bug while fixing a memory leak with the date picker. This commit sets the current instance to null,

this._curInst = null;
, to remove the date picker logic but fails to remove the UI widget.

Our proposed fix is to hide the date picker when the destroy function using $.datepicker._hideDatepicker(); so the user can no longer hover over a "ghost" date picker that can no longer be interacted with.

Co-authored-by: DawnSovereign <jason.t.kim2@gmail.com>
Copy link

linux-foundation-easycla bot commented Jul 10, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

@porterclev
Copy link
Contributor Author

@fnagel

Copy link
Member

@mgol mgol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

If the DOM is not getting destroyed, we should make sure it is after the fix. Is just hiding enough to achieve that?

I'd also want to understand fully why just setting this._curInst to null caused this issue; without this, the fix may be incomplete. Can you analyze this & report back?

Also, we'll need tests.

Co-authored-by: DawnSovereign <jason.t.kim2@gmail.com>
@DawnSovereign
Copy link

DawnSovereign commented Jul 15, 2024

The following text is the understanding we have of what happened. The date picker was getting destroyed, but a lingering reference to it was kept alive (curInst). This meant it was a memory leak. The PR that fixed the memory leak did not handle the case where the datepicker was already open. It clears curInst but it does not actually destroy or remove the DOM element

To answer your question about setting this.curInst to null; curInst is being set to null, but the datepicker still thinks it is open, so other parts of the code still access _curInst which is why we set this.curInst to null.

In order to directly address how we could get rid of the daterpicker UI that's still open, we get rid of the div instance by destroying it from the DOM and clearing dp div by setting it to null.

Here are the screenshots with our fix for before and after the div instance is destroyed:
Screenshot 2024-07-15 100202
Screenshot 2024-07-15 100147

Here is a screenshot of the stack trace:
image (1)
image

@porterclev porterclev requested a review from mgol July 15, 2024 17:43
@OmarShehata
Copy link

Here is a screenshot of the stack trace:

@DawnSovereign are you able to get this stack trace with the local, unminified code? or click on it and see what function it's in, and find the corresponding function in the source code?

from the screenshot, it looks like it's this function, right?

function datepicker_handleMouseover() {
if ( !$.datepicker._isDisabledDatepicker( datepicker_instActive.inline ? datepicker_instActive.dpDiv.parent()[ 0 ] : datepicker_instActive.input[ 0 ] ) ) {
$( this ).parents( ".ui-datepicker-calendar" ).find( "a" ).removeClass( "ui-state-hover" );
$( this ).addClass( "ui-state-hover" );
if ( this.className.indexOf( "ui-datepicker-prev" ) !== -1 ) {
$( this ).addClass( "ui-datepicker-prev-hover" );
}
if ( this.className.indexOf( "ui-datepicker-next" ) !== -1 ) {
$( this ).addClass( "ui-datepicker-next-hover" );
}
}
}

Copy link
Member

@mgol mgol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll review the code logic in more detail later but I'll reiterate this needs tests before we can merge anything.

@mgol
Copy link
Member

mgol commented Jul 22, 2024

You can also see that existing tests are not passing, please make sure they do.

Co-authored-by: DawnSovereign <jason.t.kim2@gmail.com>
@porterclev
Copy link
Contributor Author

@mgol Thanks for taking a look. We've pushed a unit test that reproduces the issue. It fails in main and passes in this branch with the fix.

We've reverted the fix back to hiding the datepicker instead of destroying the div. This is because several tests expect to re-use the element after it is destroyed. If we want to properly remove the element when the datepicker is destroyed, it looks like this may be a breaking change? I can open a ticket for that separately with more details.

The underlying issue is that destroying the datepicker by itself does not hide it. The current tests in main don't show this problem because they always hide the datepicker first before destroying it. So it is possible for a user to work around this problem at the moment by hiding before destroying.

The long term solution is to remove the element when the datepicker is destroyed, and update the tests, and mark this as a (potential?) breaking change.

Co-authored-by: DawnSovereign <jason.t.kim2@gmail.com>
@porterclev porterclev marked this pull request as ready for review July 24, 2024 19:36
Copy link
Member

@mgol mgol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. @fnagel can you have a look as well?

@mgol mgol requested a review from fnagel July 26, 2024 15:46
@mgol mgol added this to the 1.14.0 milestone Jul 26, 2024
tests/unit/datepicker/methods.js Outdated Show resolved Hide resolved
@fnagel
Copy link
Member

fnagel commented Jul 31, 2024

At first I thought that just hiding the elements instead of destroying sounds not ideal, but then I've seen the comment about "This is because several tests expect to re-use the element after it is destroyed." so the change makes sense to me.

It's not ideal that the destroy method does not really destroy everything (elements, events, ...) like other widgets do, but the Datepicker does not use the widget factory and always was a little special.

So to me, this change improves the current state and fixes a bug introduces with 1.13.x. Only the test comment is a little confusing (see my review).

@mgol We might want to squash the commits when merging this.

@mgol
Copy link
Member

mgol commented Aug 1, 2024

@fnagel Was the comment your only concern to address before merging? I get your comment about destroying; I had a look at the code and it seemed not that easy to retrofit it into the existing design. _hideDatepicker may use an animation and we'd have to destroy only when it's done, but it doesn't provide any hook to do so. We'd have to add one, then add tests, also fix existing tests to not depend on it existing... Maybe let's leave it as it is for now.

Please approve if you have no further concerns to address.

@mgol mgol requested a review from fnagel August 1, 2024 09:15
@fnagel
Copy link
Member

fnagel commented Aug 1, 2024

@mgol I'm fine with merging it as it is. Datepicker was always special (as mentioned before, no widget factory in use) and SHOULD have been replaced with the new calendar and datepicker widget but never has.

Not sure what little surprises will come up when we start changing how the structure and design currently looks ;-)

@mgol
Copy link
Member

mgol commented Aug 1, 2024

Just to make sure, I checked myself what was the cause of the original issue. Datepicker attaches a global mousedown on document:

$( document ).on( "mousedown", $.datepicker._checkExternalClick );

It never removes it. This handler hides the datepicker in certain conditions - but the first thing it checks is whether $.datepicker._curInst is truthy:
/* Close date picker if clicked elsewhere. */
_checkExternalClick: function( event ) {
if ( !$.datepicker._curInst ) {
return;
}
var $target = $( event.target ),
inst = $.datepicker._getInst( $target[ 0 ] );
if ( ( ( $target[ 0 ].id !== $.datepicker._mainDivId &&
$target.parents( "#" + $.datepicker._mainDivId ).length === 0 &&
!$target.hasClass( $.datepicker.markerClassName ) &&
!$target.closest( "." + $.datepicker._triggerClass ).length &&
$.datepicker._datepickerShowing && !( $.datepicker._inDialog && $.blockUI ) ) ) ||
( $target.hasClass( $.datepicker.markerClassName ) && $.datepicker._curInst !== inst ) ) {
$.datepicker._hideDatepicker();
}
},

Since #1852 made _curInst cleared, this method stopped doing anything.

Some parts of the description from #2178 are incorrect, though - in 1.12.1, the datepicker UI was not hidden on destroy either! It was just hidden after the first mousedown. If you just hovered over the datepicker or clicked on any of its elements, you'd get lots of console errors even in 1.12.1.

Therefore, this PR actually changes the behavior compared to 1.12.1 which was already partially broken. I think it's a reasonable change - the datepicker UI should hide when destroying the widget - but let's call it out.

@mgol mgol changed the title Datepicker: hides widget when destroyed Datepicker: Hide the UI on destroy Aug 5, 2024
@mgol mgol merged commit 02a6e6b into jquery:main Aug 5, 2024
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

5 participants