Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import * as React from 'react';
import { spy } from 'sinon';
import {
screen,
fireEvent,
createEvent,
within,
fireTouchChangedEvent,
waitFor,
} from '@mui/internal-test-utils';
import { screen, fireEvent, within, waitFor } from '@mui/internal-test-utils';
import {
adapterToUse,
buildPickerDragInteractions,
rangeCalendarDayTouches,
executeDateDrag,
executeDateDragWithoutDrop,
createPickerRenderer,
} from 'test/utils/pickers';
import { MockedDataTransfer } from 'test/utils/dragAndDrop';
import {
DateRangeCalendar,
dateRangeCalendarClasses as classes,
Expand Down Expand Up @@ -128,43 +120,6 @@ describe('<DateRangeCalendar />', () => {
});

describe('dragging behavior', () => {
let dataTransfer: DataTransfer | null;

const { executeDateDragWithoutDrop, executeDateDrag } = buildPickerDragInteractions(
() => dataTransfer,
);

type TouchTarget = Pick<Touch, 'clientX' | 'clientY'>;

const fireTouchEvent = (
type: 'touchstart' | 'touchmove' | 'touchend',
target: Element,
touch: TouchTarget,
) => {
fireTouchChangedEvent(target, type, { changedTouches: [touch] });
};

const executeDateTouchDragWithoutEnd = (target: Element, ...touchTargets: TouchTarget[]) => {
fireTouchEvent('touchstart', target, touchTargets[0]);
touchTargets.slice(0, touchTargets.length - 1).forEach((touch) => {
fireTouchEvent('touchmove', target, touch);
});
};

const executeDateTouchDrag = (target: Element, ...touchTargets: TouchTarget[]) => {
const endTouchTarget = touchTargets[touchTargets.length - 1];
executeDateTouchDragWithoutEnd(target, ...touchTargets);
fireTouchEvent('touchend', target, endTouchTarget);
};

beforeEach(() => {
dataTransfer = new MockedDataTransfer();
});

afterEach(() => {
dataTransfer = null;
});

it('should not emit "onChange" when dragging is ended where it was started', () => {
const onChange = spy();
render(
Expand All @@ -183,31 +138,6 @@ describe('<DateRangeCalendar />', () => {
expect(onChange.callCount).to.equal(0);
});

it.skipIf(!document.elementFromPoint)(
'should not emit "onChange" when touch dragging is ended where it was started',
() => {
const onChange = spy();
render(
<DateRangeCalendar
onChange={onChange}
defaultValue={[adapterToUse.date('2018-01-01'), adapterToUse.date('2018-01-10')]}
/>,
);

const startDay = screen.getByRole('gridcell', { name: '1', selected: true });
expect(onChange.callCount).to.equal(0);

executeDateTouchDrag(
startDay,
rangeCalendarDayTouches['2018-01-01'],
rangeCalendarDayTouches['2018-01-02'],
rangeCalendarDayTouches['2018-01-01'],
);

expect(onChange.callCount).to.equal(0);
},
);

it('should emit "onChange" when dragging end date', () => {
const onChange = spy();
const initialValue: [any, any] = [
Expand Down Expand Up @@ -251,51 +181,6 @@ describe('<DateRangeCalendar />', () => {
expect(document.activeElement).toHaveAccessibleName('2');
});

it.skipIf(!document.elementFromPoint)(
'should emit "onChange" when touch dragging end date',
() => {
const onChange = spy();
const initialValue: [any, any] = [
adapterToUse.date('2018-01-02'),
adapterToUse.date('2018-01-11'),
];
render(<DateRangeCalendar onChange={onChange} defaultValue={initialValue} />);

// test range reduction
executeDateTouchDrag(
getPickerDay('11'),
rangeCalendarDayTouches['2018-01-11'],
rangeCalendarDayTouches['2018-01-10'],
);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0][0]).toEqualDateTime(initialValue[0]);
expect(onChange.lastCall.args[0][1]).toEqualDateTime(new Date(2018, 0, 10));

// test range expansion
executeDateTouchDrag(
getPickerDay('10'),
rangeCalendarDayTouches['2018-01-10'],
rangeCalendarDayTouches['2018-01-11'],
);

expect(onChange.callCount).to.equal(2);
expect(onChange.lastCall.args[0][0]).toEqualDateTime(initialValue[0]);
expect(onChange.lastCall.args[0][1]).toEqualDateTime(initialValue[1]);

// test range flip
executeDateTouchDrag(
getPickerDay('11'),
rangeCalendarDayTouches['2018-01-11'],
rangeCalendarDayTouches['2018-01-01'],
);

expect(onChange.callCount).to.equal(3);
expect(onChange.lastCall.args[0][0]).toEqualDateTime(new Date(2018, 0, 1));
expect(onChange.lastCall.args[0][1]).toEqualDateTime(initialValue[0]);
},
);

it('should emit "onChange" when dragging start date', () => {
const onChange = spy();
const initialValue: [any, any] = [
Expand Down Expand Up @@ -329,51 +214,6 @@ describe('<DateRangeCalendar />', () => {
expect(document.activeElement).toHaveAccessibleName('22');
});

it.skipIf(!document.elementFromPoint)(
'should emit "onChange" when touch dragging start date',
() => {
const onChange = spy();
const initialValue: [any, any] = [
adapterToUse.date('2018-01-01'),
adapterToUse.date('2018-01-10'),
];
render(<DateRangeCalendar onChange={onChange} defaultValue={initialValue} />);

// test range reduction
executeDateTouchDrag(
getPickerDay('1'),
rangeCalendarDayTouches['2018-01-01'],
rangeCalendarDayTouches['2018-01-02'],
);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0][0]).toEqualDateTime(new Date(2018, 0, 2));
expect(onChange.lastCall.args[0][1]).toEqualDateTime(initialValue[1]);

// test range expansion
executeDateTouchDrag(
getPickerDay('2'),
rangeCalendarDayTouches['2018-01-02'],
rangeCalendarDayTouches['2018-01-01'],
);

expect(onChange.callCount).to.equal(2);
expect(onChange.lastCall.args[0][0]).toEqualDateTime(initialValue[0]);
expect(onChange.lastCall.args[0][1]).toEqualDateTime(initialValue[1]);

// test range flip
executeDateTouchDrag(
getPickerDay('1'),
rangeCalendarDayTouches['2018-01-01'],
rangeCalendarDayTouches['2018-01-11'],
);

expect(onChange.callCount).to.equal(3);
expect(onChange.lastCall.args[0][0]).toEqualDateTime(initialValue[1]);
expect(onChange.lastCall.args[0][1]).toEqualDateTime(new Date(2018, 0, 11));
},
);

it('should dynamically update "shouldDisableDate" when flip dragging', () => {
const initialValue: [any, any] = [
adapterToUse.date('2018-01-01'),
Expand Down Expand Up @@ -404,44 +244,10 @@ describe('<DateRangeCalendar />', () => {
).to.have.lengthOf(10);
});

it.skipIf(!document.elementFromPoint)(
'should dynamically update "shouldDisableDate" when flip touch dragging',
() => {
const initialValue: [any, any] = [
adapterToUse.date('2018-01-01'),
adapterToUse.date('2018-01-07'),
];
render(
<DateRangeCalendar
defaultValue={initialValue}
shouldDisableDate={dynamicShouldDisableDate}
calendars={1}
/>,
);

expect(screen.getByRole('gridcell', { name: '5' })).to.have.attribute('disabled');
expect(
screen.getAllByRole<HTMLButtonElement>('gridcell').filter((c) => c.disabled),
).to.have.lengthOf(6);
// flip date range
executeDateTouchDragWithoutEnd(
screen.getByRole('gridcell', { name: '1' }),
rangeCalendarDayTouches['2018-01-01'],
rangeCalendarDayTouches['2018-01-09'],
rangeCalendarDayTouches['2018-01-10'],
);

expect(screen.getByRole('gridcell', { name: '9' })).to.have.attribute('disabled');
expect(
screen.getAllByRole<HTMLButtonElement>('gridcell').filter((c) => c.disabled),
).to.have.lengthOf(10);
},
);

it('should handle drag events targeting child elements inside the day button', () => {
// This test validates the fix for when drag events target child elements (e.g., text spans)
// inside the day button, rather than the button itself. The fix uses .closest() to find
// the ancestor with the data-timestamp attribute.
it('should handle pointer events targeting child elements inside the day button', () => {
// Real browsers can route pointer events to child nodes (text span, ripple)
// when the user touches inside the day button. The handler must walk up to
// the button to read its data attributes — exercise that path explicitly.
const onChange = spy();
const initialValue: [PickerValidDate, PickerValidDate] = [
adapterToUse.date('2018-01-10'),
Expand All @@ -452,31 +258,14 @@ describe('<DateRangeCalendar />', () => {
const startDayButton = screen.getByRole('gridcell', { name: '31', selected: true });
const endDayButton = screen.getByRole('gridcell', { name: '29' });

// Create synthetic child elements inside the buttons to simulate the real browser scenario
// where drag events can target child elements (e.g., text spans, TouchRipple).
// This ensures the `.closest()` fallback path is exercised.
const startDayChild = document.createElement('span');
startDayButton.appendChild(startDayChild);
const endDayChild = document.createElement('span');
endDayButton.appendChild(endDayChild);

// Execute drag using child elements as targets
// This simulates a user clicking on the day number text or ripple effect
const createDragEventOnChild = (
type: 'dragStart' | 'dragEnter' | 'dragOver' | 'drop' | 'dragEnd' | 'dragLeave',
target: Element,
) => {
const createdEvent = createEvent[type](target);
Object.defineProperty(createdEvent, 'dataTransfer', { value: dataTransfer });
return createdEvent;
};

fireEvent(startDayChild, createDragEventOnChild('dragStart', startDayChild));
fireEvent(startDayChild, createDragEventOnChild('dragLeave', startDayChild));
fireEvent(endDayChild, createDragEventOnChild('dragEnter', endDayChild));
fireEvent(endDayChild, createDragEventOnChild('dragOver', endDayChild));
fireEvent(endDayChild, createDragEventOnChild('drop', endDayChild));
fireEvent(endDayChild, createDragEventOnChild('dragEnd', endDayChild));
fireEvent.pointerDown(startDayChild, { pointerId: 1, button: 0, isPrimary: true });
fireEvent.pointerOver(endDayChild, { pointerId: 1 });
fireEvent.pointerUp(document, { pointerId: 1 });

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0][0]).toEqualDateTime(initialValue[0]);
Expand Down
Loading
Loading