@mui/x-date-pickers-pro 9.2.0 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/DateRangeCalendar/DateRangeCalendar.js +2 -2
- package/DateRangeCalendar/DateRangeCalendar.mjs +2 -2
- package/DateRangeCalendar/useDragRange.d.mts +3 -11
- package/DateRangeCalendar/useDragRange.d.ts +3 -11
- package/DateRangeCalendar/useDragRange.js +264 -196
- package/DateRangeCalendar/useDragRange.mjs +265 -197
- package/DateRangePickerDay/DateRangePickerDay.js +10 -5
- package/DateRangePickerDay/DateRangePickerDay.mjs +10 -5
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.js +2 -2
- package/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.mjs +2 -2
- package/internals/hooks/useMobileRangePicker/useMobileRangePicker.js +2 -2
- package/internals/hooks/useMobileRangePicker/useMobileRangePicker.mjs +2 -2
- package/package.json +2 -2
|
@@ -12,16 +12,10 @@ var React = _interopRequireWildcard(require("react"));
|
|
|
12
12
|
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
|
|
13
13
|
var _domUtils = require("@mui/x-internals/domUtils");
|
|
14
14
|
var _dateUtils = require("../internals/utils/date-utils");
|
|
15
|
-
const isEnabledButtonElement = element => (0, _domUtils.isHTMLElement)(element) && element.tagName === 'BUTTON' && !element.disabled;
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @param element The element to start searching from.
|
|
22
|
-
* @param dataAttribute The data attribute name — must be a single lowercase word
|
|
23
|
-
* (e.g., 'timestamp', 'position') because `dataset[attr]` uses camelCase
|
|
24
|
-
* while `.closest()` uses kebab-case, and these only align for single-word names.
|
|
16
|
+
* Returns the element (or its closest ancestor) carrying `data-{attr}`.
|
|
17
|
+
* Single-word `attr` only — `dataset[attr]` (camelCase) and `.closest()`
|
|
18
|
+
* (kebab-case) only agree for single-word names.
|
|
25
19
|
*/
|
|
26
20
|
const getClosestElementWithDataAttribute = (element, dataAttribute) => {
|
|
27
21
|
if (!element) {
|
|
@@ -38,95 +32,40 @@ const resolveDateFromTarget = (target, adapter, timezone) => {
|
|
|
38
32
|
if (!timestampString) {
|
|
39
33
|
return null;
|
|
40
34
|
}
|
|
41
|
-
const timestamp = Number(timestampString);
|
|
42
|
-
return adapter.date(new Date(timestamp).toISOString(), timezone);
|
|
43
|
-
};
|
|
44
|
-
const isSameAsDraggingDate = event => {
|
|
45
|
-
const target = (0, _domUtils.getTarget)(event.nativeEvent);
|
|
46
|
-
if (!(0, _domUtils.isHTMLElement)(target)) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
const element = getClosestElementWithDataAttribute(target, 'timestamp');
|
|
50
|
-
return element?.dataset.timestamp === event.dataTransfer.getData('draggingDate');
|
|
51
|
-
};
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
const resolveButtonElement = element => {
|
|
60
|
-
if (!element) {
|
|
36
|
+
// Guard against malformed `data-timestamp` — `Number('abc')` is `NaN` and
|
|
37
|
+
// `new Date(NaN).toISOString()` throws, which would otherwise wedge the
|
|
38
|
+
// gesture mid-`pointerover`.
|
|
39
|
+
const timestamp = Number(timestampString);
|
|
40
|
+
if (!Number.isFinite(timestamp)) {
|
|
61
41
|
return null;
|
|
62
42
|
}
|
|
63
|
-
|
|
64
|
-
// Check if element itself is a valid button
|
|
65
|
-
if (isEnabledButtonElement(element)) {
|
|
66
|
-
return element;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Search upward - element could be a child of the button (e.g., text span, TouchRipple)
|
|
70
|
-
const closestButton = element.closest('button');
|
|
71
|
-
if (isEnabledButtonElement(closestButton)) {
|
|
72
|
-
return closestButton;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Search downward (breadth-first, max 3 levels) - element could be a wrapper containing the button.
|
|
76
|
-
// Day cells have shallow DOM, so a small depth limit keeps this efficient.
|
|
77
|
-
const queue = Array.from(element.children).map(el => ({
|
|
78
|
-
el,
|
|
79
|
-
depth: 1
|
|
80
|
-
}));
|
|
81
|
-
const maxDepth = 3;
|
|
82
|
-
while (queue.length > 0) {
|
|
83
|
-
const {
|
|
84
|
-
el: current,
|
|
85
|
-
depth
|
|
86
|
-
} = queue.shift();
|
|
87
|
-
if (isEnabledButtonElement(current)) {
|
|
88
|
-
return current;
|
|
89
|
-
}
|
|
90
|
-
if (depth < maxDepth) {
|
|
91
|
-
queue.push(...Array.from(current.children).map(el => ({
|
|
92
|
-
el,
|
|
93
|
-
depth: depth + 1
|
|
94
|
-
})));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
};
|
|
99
|
-
const resolveElementFromTouch = (event, ignoreTouchTarget) => {
|
|
100
|
-
// don't parse multi-touch result
|
|
101
|
-
if (event.changedTouches?.length === 1 && event.touches.length <= 1) {
|
|
102
|
-
const element = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
|
|
103
|
-
// `elementFromPoint` could have resolved preview div or wrapping div
|
|
104
|
-
// might need to recursively find the nested button
|
|
105
|
-
const buttonElement = resolveButtonElement(element);
|
|
106
|
-
if (ignoreTouchTarget && buttonElement === event.changedTouches[0].target) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
return buttonElement;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
43
|
+
return adapter.date(new Date(timestamp).toISOString(), timezone);
|
|
112
44
|
};
|
|
113
45
|
const useDragRangeEvents = ({
|
|
114
46
|
adapter,
|
|
115
47
|
setRangeDragDay,
|
|
116
48
|
setIsDragging,
|
|
117
|
-
isDragging,
|
|
118
49
|
onDatePositionChange,
|
|
119
50
|
onDrop,
|
|
120
51
|
disableDragEditing,
|
|
121
52
|
dateRange,
|
|
122
53
|
timezone
|
|
123
54
|
}) => {
|
|
124
|
-
const
|
|
125
|
-
React.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
55
|
+
const isDraggingRef = React.useRef(false);
|
|
56
|
+
const pointerIdRef = React.useRef(null);
|
|
57
|
+
const sourceDateRef = React.useRef(null);
|
|
58
|
+
const sourcePositionRef = React.useRef(null);
|
|
59
|
+
const didMoveRef = React.useRef(false);
|
|
60
|
+
// Last cell the pointer hovered. Used to dedupe `pointerover` (which fires
|
|
61
|
+
// repeatedly within the same cell), and as the drop fallback for
|
|
62
|
+
// `pointercancel` (whose `event.target` is unreliable across browsers).
|
|
63
|
+
const lastHoveredCellRef = React.useRef(null);
|
|
64
|
+
// Each entry removes one document listener the gesture installed.
|
|
65
|
+
const listenerCleanupsRef = React.useRef([]);
|
|
66
|
+
// Outstanding capture-phase click suppressor, if any. Tracked so back-to-back
|
|
67
|
+
// drags can tear the prior one down before installing a new one.
|
|
68
|
+
const clickSuppressorRef = React.useRef(null);
|
|
130
69
|
const isElementDraggable = day => {
|
|
131
70
|
if (day == null) {
|
|
132
71
|
return false;
|
|
@@ -136,146 +75,276 @@ const useDragRangeEvents = ({
|
|
|
136
75
|
const isSelectedEndDate = (0, _dateUtils.isEndOfRange)(adapter, day, dateRange);
|
|
137
76
|
return shouldInitDragging && (isSelectedStartDate || isSelectedEndDate);
|
|
138
77
|
};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
78
|
+
|
|
79
|
+
// Resets every ref the gesture mutated and removes any listeners installed
|
|
80
|
+
// during the gesture. Safe to call from event handlers and from unmount.
|
|
81
|
+
// Only reads refs, so its closure never changes — memoized for stable
|
|
82
|
+
// identity (referenced by `cleanup` and the unmount effect).
|
|
83
|
+
const clearGestureState = (0, _useEventCallback.default)(() => {
|
|
84
|
+
isDraggingRef.current = false;
|
|
85
|
+
pointerIdRef.current = null;
|
|
86
|
+
sourceDateRef.current = null;
|
|
87
|
+
sourcePositionRef.current = null;
|
|
88
|
+
didMoveRef.current = false;
|
|
89
|
+
lastHoveredCellRef.current = null;
|
|
90
|
+
listenerCleanupsRef.current.forEach(teardown => teardown());
|
|
91
|
+
listenerCleanupsRef.current = [];
|
|
92
|
+
// Also tear down any in-flight click suppressor — without this, an
|
|
93
|
+
// unmount in the brief window between `pointerup` and the
|
|
94
|
+
// `setTimeout(0)` teardown leaves a capture-phase listener attached
|
|
95
|
+
// to `document` that swallows the next click on unrelated UI.
|
|
96
|
+
clickSuppressorRef.current?.();
|
|
97
|
+
});
|
|
98
|
+
const cleanup = (0, _useEventCallback.default)(() => {
|
|
99
|
+
const wasActive = didMoveRef.current;
|
|
100
|
+
clearGestureState();
|
|
101
|
+
// A press without movement never activated drag UI, so skip the re-render.
|
|
102
|
+
if (wasActive) {
|
|
103
|
+
setIsDragging(false);
|
|
104
|
+
setRangeDragDay(null);
|
|
143
105
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
106
|
+
});
|
|
107
|
+
const installClickSuppressor = doc => {
|
|
108
|
+
// Tear down a prior outstanding suppressor first; back-to-back drags
|
|
109
|
+
// would otherwise race two listeners on the document.
|
|
110
|
+
clickSuppressorRef.current?.();
|
|
111
|
+
|
|
112
|
+
// suppress and teardown reference each other, so forward-declare suppress.
|
|
113
|
+
let suppress;
|
|
114
|
+
const teardown = () => {
|
|
115
|
+
doc.removeEventListener('click', suppress, {
|
|
116
|
+
capture: true
|
|
117
|
+
});
|
|
118
|
+
if (clickSuppressorRef.current === teardown) {
|
|
119
|
+
clickSuppressorRef.current = null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
suppress = clickEvent => {
|
|
123
|
+
clickEvent.preventDefault();
|
|
124
|
+
// `stopImmediatePropagation` (rather than just `stopPropagation`) so
|
|
125
|
+
// other capture-phase click listeners on `document` — analytics, focus
|
|
126
|
+
// traps, third-party overlays — don't observe the synthesized
|
|
127
|
+
// post-drag click as if the user intentionally clicked the cell.
|
|
128
|
+
clickEvent.stopImmediatePropagation();
|
|
129
|
+
teardown();
|
|
130
|
+
};
|
|
131
|
+
doc.addEventListener('click', suppress, {
|
|
132
|
+
capture: true
|
|
133
|
+
});
|
|
134
|
+
clickSuppressorRef.current = teardown;
|
|
135
|
+
// If no click ever fires (drop on a different cell, browser doesn't
|
|
136
|
+
// synthesize), tear the listener down so it doesn't leak.
|
|
137
|
+
setTimeout(teardown, 0);
|
|
138
|
+
};
|
|
139
|
+
const finalizeGesture = (event, ownerDoc, eventType) => {
|
|
140
|
+
const wasMoved = didMoveRef.current;
|
|
141
|
+
const sourceDate = sourceDateRef.current;
|
|
142
|
+
|
|
143
|
+
// For `pointerup`, the drop target is whatever element the pointer was
|
|
144
|
+
// actually over at release time — releasing into a gap or off the calendar
|
|
145
|
+
// resolves to `null` and cancels, matching native HTML5 drag.
|
|
146
|
+
// For `pointercancel`, `event.target` can be unreliable (browsers vary on
|
|
147
|
+
// whether it's the current under-pointer element or the gesture's start
|
|
148
|
+
// element). Fall back to the last cell the user hovered, which is the
|
|
149
|
+
// closest expression of their intent.
|
|
150
|
+
let dropOrigin;
|
|
151
|
+
if (eventType === 'pointercancel') {
|
|
152
|
+
dropOrigin = lastHoveredCellRef.current;
|
|
153
|
+
} else {
|
|
154
|
+
dropOrigin = event.target instanceof HTMLElement ? event.target : null;
|
|
147
155
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
const dropCell = getClosestElementWithDataAttribute(dropOrigin, 'timestamp');
|
|
157
|
+
const newDate = dropCell ? resolveDateFromTarget(dropCell, adapter, timezone) : null;
|
|
158
|
+
// Resolve the focusable `<button>` separately from `dropCell`. Today
|
|
159
|
+
// `data-timestamp` lives on the button itself, but a future custom slot
|
|
160
|
+
// could put it on a wrapper; in that case the cell isn't focusable and
|
|
161
|
+
// the disabled state lives on the inner button.
|
|
162
|
+
const dropButton = dropOrigin?.closest('button') ?? dropCell?.querySelector('button') ?? null;
|
|
163
|
+
|
|
164
|
+
// `shouldDisableDate` / min-max / readOnly mark the day's button as
|
|
165
|
+
// `disabled`. `pointerup` still lands on a disabled `<button>` in
|
|
166
|
+
// Chromium/WebKit, so guard explicitly — `DateRangeCalendar.handleDrop`
|
|
167
|
+
// doesn't re-validate the date.
|
|
168
|
+
const isDropDisabled = dropButton?.disabled === true;
|
|
169
|
+
cleanup();
|
|
170
|
+
if (eventType === 'pointerup' && wasMoved && dropCell) {
|
|
171
|
+
// The click that follows pointerup on a day cell would re-enter the
|
|
172
|
+
// day's selection logic and undo the drop (or, when the drag returned
|
|
173
|
+
// to the source, replace the range with a single-day selection).
|
|
174
|
+
// Swallow it. Gated on `dropCell` so a release outside the calendar
|
|
175
|
+
// doesn't swallow an unrelated click on the host UI.
|
|
176
|
+
installClickSuppressor(ownerDoc);
|
|
157
177
|
}
|
|
158
|
-
if (
|
|
159
|
-
|
|
178
|
+
if (wasMoved && newDate && sourceDate && !isDropDisabled && !adapter.isEqual(newDate, sourceDate)) {
|
|
179
|
+
dropButton?.focus();
|
|
180
|
+
onDrop(newDate);
|
|
160
181
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// `touchmove`-blocks-scroll listener. Attached eagerly in
|
|
185
|
+
// `handlePointerDown` for touch pointers only. Mouse/pen don't fire touch
|
|
186
|
+
// events. Stable at hook level so the listener identity is consistent
|
|
187
|
+
// across renders.
|
|
188
|
+
const onTouchMove = (0, _useEventCallback.default)(touchEvent => {
|
|
189
|
+
if (isDraggingRef.current) {
|
|
190
|
+
// `touch-action: none` on the source cell isn't enough once the finger
|
|
191
|
+
// crosses cell boundaries.
|
|
192
|
+
touchEvent.preventDefault();
|
|
166
193
|
}
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
});
|
|
195
|
+
const handlePointerDown = (0, _useEventCallback.default)(event => {
|
|
196
|
+
// Ignore secondary mouse buttons (middle = 1, right = 2). `> 0` rather
|
|
197
|
+
// than `!== 0` keeps the gesture permissive when `event.button` is left
|
|
198
|
+
// unset by a synthetic event (some test environments).
|
|
199
|
+
if (event.button > 0) {
|
|
169
200
|
return;
|
|
170
201
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
202
|
+
|
|
203
|
+
// Secondary multi-touch pointers (second finger, etc.) are explicitly
|
|
204
|
+
// not-primary; let them pass through without disturbing the active gesture.
|
|
205
|
+
if (event.isPrimary === false) {
|
|
175
206
|
return;
|
|
176
207
|
}
|
|
177
|
-
event.
|
|
178
|
-
|
|
179
|
-
event.dataTransfer.dropEffect = 'move';
|
|
180
|
-
setRangeDragDay(resolveDateFromTarget((0, _domUtils.getTarget)(event.nativeEvent), adapter, timezone));
|
|
181
|
-
});
|
|
182
|
-
const handleTouchMove = (0, _useEventCallback.default)(event => {
|
|
183
|
-
const target = resolveElementFromTouch(event);
|
|
184
|
-
if (!target) {
|
|
208
|
+
const newDate = resolveDateFromTarget(event.currentTarget, adapter, timezone);
|
|
209
|
+
if (!isElementDraggable(newDate)) {
|
|
185
210
|
return;
|
|
186
211
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
|
|
213
|
+
// A fresh primary pointerdown definitionally ends any previous gesture
|
|
214
|
+
// (covers pen+touch, where each pointer type has its own primary, and
|
|
215
|
+
// the recovery case where the original gesture's `pointerup` was lost).
|
|
216
|
+
if (pointerIdRef.current != null) {
|
|
217
|
+
cleanup();
|
|
190
218
|
}
|
|
191
219
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
220
|
+
// Touch implicitly captures the pointer on `pointerdown`, pinning all
|
|
221
|
+
// subsequent events to the source. Release so sibling cells receive their
|
|
222
|
+
// own `pointerover`. jsdom lacks the API; Safari 15 / some Android WebViews
|
|
223
|
+
// race between the `hasPointerCapture` check and the release call and throw
|
|
224
|
+
// `InvalidPointerId` — benign, swallow it.
|
|
225
|
+
try {
|
|
226
|
+
if (typeof event.currentTarget.hasPointerCapture === 'function' && event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
227
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// already released, nothing to do
|
|
196
231
|
}
|
|
197
232
|
|
|
198
|
-
//
|
|
199
|
-
|
|
233
|
+
// Note: deliberately not calling `event.preventDefault()` here. Doing so
|
|
234
|
+
// would suppress the synthesized click that follows pointerup, which is
|
|
235
|
+
// load-bearing for tap-to-advance on an endpoint cell. The iOS magnifier
|
|
236
|
+
// is held off by `touch-action: none` + `user-select: none` on the cell.
|
|
200
237
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
238
|
+
pointerIdRef.current = event.pointerId;
|
|
239
|
+
isDraggingRef.current = true;
|
|
240
|
+
sourceDateRef.current = newDate;
|
|
241
|
+
didMoveRef.current = false;
|
|
242
|
+
lastHoveredCellRef.current = event.currentTarget;
|
|
243
|
+
|
|
244
|
+
// Walk up rather than reading `currentTarget.dataset` directly so the
|
|
245
|
+
// hook keeps working if a future slot puts `data-position` on a wrapper
|
|
246
|
+
// around the cell (mirrors how we resolve `data-timestamp`).
|
|
247
|
+
const positionHost = getClosestElementWithDataAttribute(event.currentTarget, 'position');
|
|
248
|
+
sourcePositionRef.current = positionHost?.dataset.position ?? null;
|
|
249
|
+
|
|
250
|
+
// Use the owner document (matters for iframe-hosted pickers) for all
|
|
251
|
+
// document-level listeners.
|
|
252
|
+
const ownerDoc = event.currentTarget.ownerDocument ?? document;
|
|
253
|
+
|
|
254
|
+
// Drag UI activation is deferred until the first real move — a pure
|
|
255
|
+
// press on an endpoint must leave selection state alone so the click
|
|
256
|
+
// handler can advance it normally.
|
|
257
|
+
|
|
258
|
+
const onPointerUp = pointerEvent => {
|
|
259
|
+
if (pointerEvent.pointerId !== pointerIdRef.current) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
finalizeGesture(pointerEvent, ownerDoc, 'pointerup');
|
|
263
|
+
};
|
|
264
|
+
const onPointerCancel = pointerEvent => {
|
|
265
|
+
if (pointerEvent.pointerId !== pointerIdRef.current) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Spec intent of `pointercancel` is "UA interrupted, not the user".
|
|
269
|
+
// After real movement, commit the drop the user worked for; the snap-back
|
|
270
|
+
// would otherwise be silent and inexplicable.
|
|
271
|
+
finalizeGesture(pointerEvent, ownerDoc, 'pointercancel');
|
|
272
|
+
};
|
|
273
|
+
const onKeyDown = keyEvent => {
|
|
274
|
+
if (keyEvent.key !== 'Escape' || !didMoveRef.current) {
|
|
275
|
+
// No visible drag to cancel. Leave the gesture intact and let
|
|
276
|
+
// Escape propagate (host modal/popover can still close on it).
|
|
277
|
+
// A press without movement behaves identically to a tap on
|
|
278
|
+
// release — letting cleanup run here would only half-collapse
|
|
279
|
+
// the gesture without suppressing the eventual tap-to-advance.
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
keyEvent.preventDefault();
|
|
283
|
+
cleanup();
|
|
284
|
+
};
|
|
285
|
+
ownerDoc.addEventListener('pointerup', onPointerUp);
|
|
286
|
+
ownerDoc.addEventListener('pointercancel', onPointerCancel);
|
|
287
|
+
ownerDoc.addEventListener('keydown', onKeyDown);
|
|
288
|
+
listenerCleanupsRef.current.push(() => ownerDoc.removeEventListener('pointerup', onPointerUp), () => ownerDoc.removeEventListener('pointercancel', onPointerCancel), () => ownerDoc.removeEventListener('keydown', onKeyDown));
|
|
289
|
+
|
|
290
|
+
// For touch input, attach the scroll-suppression listener up front rather
|
|
291
|
+
// than lazily on first movement. The Pointer Events spec latches
|
|
292
|
+
// `touch-action: none` from the source cell over the rest of the gesture,
|
|
293
|
+
// but real-world WebKit/Chromium versions don't always honor that —
|
|
294
|
+
// attaching eagerly closes that window. Mouse and pen don't fire touch
|
|
295
|
+
// events so they don't need it.
|
|
296
|
+
if (event.pointerType === 'touch') {
|
|
297
|
+
ownerDoc.addEventListener('touchmove', onTouchMove, {
|
|
298
|
+
passive: false
|
|
299
|
+
});
|
|
300
|
+
listenerCleanupsRef.current.push(() => ownerDoc.removeEventListener('touchmove', onTouchMove));
|
|
212
301
|
}
|
|
213
|
-
event.preventDefault();
|
|
214
|
-
event.stopPropagation();
|
|
215
302
|
});
|
|
216
|
-
|
|
217
|
-
|
|
303
|
+
|
|
304
|
+
// Use `pointerover` (bubbles) rather than `pointerenter`: React's
|
|
305
|
+
// `onPointerEnter` is implemented on top of over/out.
|
|
306
|
+
const handlePointerOver = (0, _useEventCallback.default)(event => {
|
|
307
|
+
if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current) {
|
|
218
308
|
return;
|
|
219
309
|
}
|
|
220
|
-
event.
|
|
221
|
-
event.stopPropagation();
|
|
222
|
-
event.dataTransfer.dropEffect = 'move';
|
|
223
|
-
});
|
|
224
|
-
const handleTouchEnd = (0, _useEventCallback.default)(event => {
|
|
225
|
-
if (!isDragging) {
|
|
310
|
+
if (lastHoveredCellRef.current === event.currentTarget) {
|
|
226
311
|
return;
|
|
227
312
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const target = resolveElementFromTouch(event, true);
|
|
231
|
-
if (!target) {
|
|
313
|
+
const newDate = resolveDateFromTarget(event.currentTarget, adapter, timezone);
|
|
314
|
+
if (!newDate) {
|
|
232
315
|
return;
|
|
233
316
|
}
|
|
317
|
+
lastHoveredCellRef.current = event.currentTarget;
|
|
318
|
+
const isDifferentFromSource = sourceDateRef.current && !adapter.isEqual(newDate, sourceDateRef.current);
|
|
319
|
+
if (!didMoveRef.current && isDifferentFromSource) {
|
|
320
|
+
// A custom day slot could strip `data-position`; without it the preview
|
|
321
|
+
// would compute against the wrong endpoint, so abort the drag rather
|
|
322
|
+
// than rendering something misleading.
|
|
323
|
+
if (!sourcePositionRef.current) {
|
|
324
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
325
|
+
console.warn('MUI X: A drag was initiated on a day cell missing `data-position`. ' + 'Drag editing requires the cell to advertise which range endpoint it represents.');
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
234
329
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
const handleDragEnd = (0, _useEventCallback.default)(event => {
|
|
243
|
-
if (!isDragging) {
|
|
244
|
-
return;
|
|
330
|
+
// First real move: activate drag UI and tell the parent which endpoint
|
|
331
|
+
// is being dragged so the preview computes against the correct side.
|
|
332
|
+
didMoveRef.current = true;
|
|
333
|
+
onDatePositionChange(sourcePositionRef.current);
|
|
334
|
+
setIsDragging(true);
|
|
245
335
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
setIsDragging(false);
|
|
249
|
-
setRangeDragDay(null);
|
|
250
|
-
});
|
|
251
|
-
const handleDrop = (0, _useEventCallback.default)(event => {
|
|
252
|
-
if (!isDragging) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
event.preventDefault();
|
|
256
|
-
event.stopPropagation();
|
|
257
|
-
setIsDragging(false);
|
|
258
|
-
setRangeDragDay(null);
|
|
259
|
-
// make sure the focused element is the element where drop ended
|
|
260
|
-
event.currentTarget.focus();
|
|
261
|
-
if (isSameAsDraggingDate(event)) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const newDate = resolveDateFromTarget((0, _domUtils.getTarget)(event.nativeEvent), adapter, timezone);
|
|
265
|
-
if (newDate) {
|
|
266
|
-
onDrop(newDate);
|
|
336
|
+
if (didMoveRef.current) {
|
|
337
|
+
setRangeDragDay(newDate);
|
|
267
338
|
}
|
|
268
339
|
});
|
|
340
|
+
|
|
341
|
+
// On unmount, clear gesture state so a remount can start fresh and any
|
|
342
|
+
// detached DOM nodes still referenced by gesture refs can be GC'd.
|
|
343
|
+
// `clearGestureState` is `useEventCallback`-stable, so the effect runs once.
|
|
344
|
+
React.useEffect(() => () => clearGestureState(), [clearGestureState]);
|
|
269
345
|
return {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
onDragLeave: handleDragLeave,
|
|
273
|
-
onDragOver: handleDragOver,
|
|
274
|
-
onDragEnd: handleDragEnd,
|
|
275
|
-
onDrop: handleDrop,
|
|
276
|
-
onTouchStart: handleTouchStart,
|
|
277
|
-
onTouchMove: handleTouchMove,
|
|
278
|
-
onTouchEnd: handleTouchEnd
|
|
346
|
+
onPointerDown: handlePointerDown,
|
|
347
|
+
onPointerOver: handlePointerOver
|
|
279
348
|
};
|
|
280
349
|
};
|
|
281
350
|
const useDragRange = ({
|
|
@@ -310,7 +379,6 @@ const useDragRange = ({
|
|
|
310
379
|
onDatePositionChange,
|
|
311
380
|
onDrop,
|
|
312
381
|
setIsDragging,
|
|
313
|
-
isDragging,
|
|
314
382
|
setRangeDragDay: handleRangeDragDayChange,
|
|
315
383
|
disableDragEditing,
|
|
316
384
|
dateRange,
|