Skip to content

[DateRangeCalendar] Ensure date dragging triggers regardless of trigger element#21868

Open
michelengelen wants to merge 6 commits intomui:masterfrom
michelengelen:bugfix/date-range-dragging-fix
Open

[DateRangeCalendar] Ensure date dragging triggers regardless of trigger element#21868
michelengelen wants to merge 6 commits intomui:masterfrom
michelengelen:bugfix/date-range-dragging-fix

Conversation

@michelengelen
Copy link
Copy Markdown
Member

This fixes the dragging of the start or end dates of a selected range.

Signed-off-by: michel <jsnerdic@gmail.com>
@michelengelen michelengelen self-assigned this Mar 26, 2026
@michelengelen michelengelen added type: bug It doesn't behave as expected. scope: pickers Changes related to the date/time pickers. needs cherry-pick The PR should be cherry-picked to master after merge. v8.x labels Mar 26, 2026
@mui-bot
Copy link
Copy Markdown

mui-bot commented Mar 26, 2026

Deploy preview: https://deploy-preview-21868--material-ui-x.netlify.app/

Bundle size report

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 🔺+656B(+0.20%) 🔺+230B(+0.28%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against 55603d1

Copy link
Copy Markdown
Member

@flaviendelangle flaviendelangle left a comment

Choose a reason for hiding this comment

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

Code Review: [DateRangeCalendar] Fix dragging start or end date

PR: #21868 by @michelengelen
Summary: Fixes drag-and-drop of start/end dates in a selected date range by making element lookups more resilient when drag events target child elements rather than the button itself.


What it does

The core issue is that drag events can target child elements inside <button> elements (e.g., a <span> inside the button), meaning event.target may not have the expected data-timestamp or data-position attributes. The fix adds .closest() fallback lookups to find the nearest ancestor with the expected dataset attribute.


Issues & Suggestions

1. Duplicated pattern — extract a helper

The "find element with dataset attribute" pattern is repeated 4 times with slight variations:

const element = target.dataset.timestamp
  ? target
  : target.closest<HTMLElement>('[data-timestamp]');

This should be extracted into a small helper like:

const closestWithData = (el: HTMLElement, attr: string) =>
  el.dataset[attr] != null ? el : el.closest<HTMLElement>(`[data-${attr}]`);

This would reduce duplication and make the intent clearer across all 4 call sites.

2. as any cast

return adapter.date(timestamp as any, timezone);

This as any is suspicious. The old code passed an ISO string (new Date(timestamp).toISOString()), the new code passes a raw number. If the adapter's date() method doesn't accept a number, this will fail at runtime while silencing the type checker. The PR should either:

  • Keep the ISO string conversion if the adapter expects a string, or
  • Fix the type signature if number is actually valid input

Either way, as any should not be the answer here.

3. resolveDateFromTargetinstanceof HTMLElement check

if (!target || !(target instanceof HTMLElement)) {
  return null;
}

This is good defensive coding. However, instanceof HTMLElement can fail across iframes/realms. If the date picker is ever rendered inside an iframe, this check would incorrectly return null. The codebase guidelines mention using owner utilities for realm-sensitive checks — worth verifying this isn't a concern here.

4. Inconsistent element resolution between resolveDateFromTarget and isSameAsDraggingDate

In resolveDateFromTarget, the function first checks instanceof HTMLElement and handles null gracefully. In isSameAsDraggingDate, there's a raw event.target as HTMLElement cast with no null/type guard. These should be consistent — if one needs the guard, both likely do.

5. onDragStart uses event.currentTarget while others use event.target

In the onDragStart handler, the code uses event.currentTarget, but in isSameAsDraggingDate it uses event.target. This inconsistency could lead to different elements being resolved depending on event bubbling. The choice between target and currentTarget should be deliberate and consistent, or at least documented with a comment explaining why they differ.

6. Missing test coverage

The PR description says "fixes dragging of start or end dates" but includes no tests. A test that simulates a drag event targeting a child element of the date button would validate the fix and prevent regression.


Verdict

The fix addresses a real bug (drag events hitting child elements), but the implementation has some rough edges: duplicated lookup patterns, an as any cast that hides a potential type mismatch, and no test coverage. Suggest addressing at least items 1, 2, and 6 before merging.


The as any is a deal breaker here, there is no way to assure that all date libraries accept a number on adapter.date.

@LukasTy LukasTy changed the title [DateRangeCalendar] Fix dragging start or end date [DateRangeCalendar] Ensure date dragging triggers regardless of trigger element Mar 27, 2026
@LukasTy LukasTy requested a review from romgrk as a code owner March 27, 2026 14:56
@LukasTy
Copy link
Copy Markdown
Member

LukasTy commented Mar 27, 2026

PR Review: [DateRangeCalendar] Ensure date dragging triggers regardless of trigger element

PR: #21868
Author: @michelengelen (with follow-up commits by @LukasTy)
Target: masterbugfix/date-range-dragging-fix
Merge Readiness Score: 9/10


Problem

Drag events on DateRangeCalendar date cells intermittently fail because event.target can resolve to a child element inside the <button> (e.g., text <span>, MUI TouchRipple) rather than the button itself. Since child elements lack data-timestamp and data-position attributes, the old code silently failed to resolve dates, causing drag to not initiate.

Solution

The fix introduces a .closest() fallback pattern: when the event target doesn't have the expected data attribute, traverse upward to find the nearest ancestor that does. This is extracted into a reusable getClosestElementWithDataAttribute helper. All event.target accesses are replaced with proper type-guarded resolution via new shared domUtils.


What Changed

File Change
useDragRange.ts Core fix — .closest() fallback for all target resolutions, hardened type guards, rewritten resolveButtonElement
x-internals/domUtils/getTarget.ts New utility — resolves event target accounting for shadow DOM (composedPath) with fallback
x-internals/domUtils/isHTMLElement.ts New utility — cross-realm safe instanceof HTMLElement check
DateRangeCalendar.test.tsx Two new tests: child-element drag + non-draggable date guard

Flavien's Review Feedback — All Addressed

# Concern Resolution
1 Extract helper for duplicated .closest() pattern getClosestElementWithDataAttribute helper extracted
2 as any cast when passing timestamp to adapter.date() Kept new Date(timestamp).toISOString() — no cast needed
3 instanceof HTMLElement fails cross-realm isHTMLElement uses ownerWindow fallback
4 Inconsistent guard between resolveDateFromTarget and isSameAsDraggingDate Both now use isHTMLElement + getClosestElementWithDataAttribute
5 Inconsistent target vs currentTarget usage Comments added; currentTarget used deliberately in handleDragStart/handleTouchMove
6 Missing test coverage Two new tests added

Analysis

Strengths

  1. Root cause correctly identified. The intermittent nature maps exactly to varying event targets depending on click position within the button — sometimes hitting the button directly, sometimes a child element.

  2. isHTMLElement is well-implemented. Cross-realm check via ownerWindow follows MUI conventions and handles iframe edge cases.

  3. resolveButtonElement rewrite is more robust. Now searches both upward (.closest('button')) and downward (BFS), covering both touch scenarios (child targets) and elementFromPoint scenarios (wrapper targets).

  4. Good separation of concerns. DOM utilities placed in x-internals/domUtils are reusable across packages. The exports: { "./*": ... } wildcard in x-internals/package.json automatically supports the new subpath.

  5. getTarget handles edge cases. The composedPath()[0] ?? event.target fallback guards against empty composed paths.

  6. Tests deterministically exercise the fix. Synthetic <span> children are appended to buttons, then drag events are fired on them — this guarantees the .closest() path is hit rather than silently falling back to the button itself.

Remaining Nits

  1. resolveButtonElement BFS is unbounded. The breadth-first downward search has no depth limit. Not a real concern for date cells (small DOM subtrees), but worth noting if this utility is later reused in deeper DOM contexts.

  2. handleDragOver behavioral change. The old code called setRangeDragDay(resolveDateFromTarget(event.target, ...)) in handleDragOver. The new code only sets dropEffect there, relying on handleDragEnter for state updates. This is correct (dragEnter fires first and is the right place for state changes), but it's an incidental behavioral change unrelated to the child-element fix — worth a note in the PR description.

Neither of these blocks merging.

Verdict

The fix correctly addresses the root cause of intermittent drag failures. All of Flavien's review feedback has been incorporated. New domUtils are clean and reusable. Tests deterministically validate the .closest() code path. Ready to merge.

@LukasTy LukasTy requested a review from flaviendelangle March 27, 2026 15:16
* @param event The event object.
* @returns The target element of the event.
*/
export function getTarget(event: Event) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is pretty much a straight copy from floating ui dom utils, and the below isHTMLElement is inspired by it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs cherry-pick The PR should be cherry-picked to master after merge. scope: pickers Changes related to the date/time pickers. type: bug It doesn't behave as expected. v8.x

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants