Skip to content
Closed
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
Expand Up @@ -133,6 +133,12 @@ class SchedulerAppointments extends CollectionWidget {
_supportedKeys() {
const parent = super._supportedKeys();

const setActiveAppointment = function ($appointment: dxElementWrapper) {
this._resetTabIndex($appointment);
// @ts-expect-error
eventsEngine.trigger($appointment, 'focus');
};

const tabHandler = function (e) {
const navigatableItems = this._getNavigatableItems();
const focusedItem = navigatableItems.filter('.dx-state-focused');
Expand All @@ -150,9 +156,225 @@ class SchedulerAppointments extends CollectionWidget {
$nextAppointment = this._getNavigatableItemByIndex(index);
}

this._resetTabIndex($nextAppointment);
// @ts-expect-error
eventsEngine.trigger($nextAppointment, 'focus');
if ($nextAppointment) {
setActiveAppointment.call(this, $nextAppointment);
}
}
};

const resizeHandler = function (e: KeyboardEvent, direction: 'up' | 'down' | 'left' | 'right') {
e.preventDefault();
e.stopPropagation();

const navigatableItems = this._getNavigatableItems();
const focusedItem = navigatableItems.filter('.dx-state-focused');

if (!focusedItem.length) {
return;
}

const $appointment = focusedItem;
if (!this.option('allowResize')) {
return;
}

const appointmentSettings = $appointment.data(APPOINTMENT_SETTINGS_KEY);
if (!appointmentSettings) {
return;
}

const isAllDay = appointmentSettings.allDay;
const renderingStrategyDirection = this.invoke('getRenderingStrategyDirection');
const isVertical = renderingStrategyDirection === 'vertical' && !isAllDay;
const isRTL = this.option('rtlEnabled');

const cellWidth = this.invoke('getCellWidth');
const cellHeight = this.invoke('getCellHeight');
const step = isVertical ? cellHeight : cellWidth;

const handles: Record<'top' | 'bottom' | 'left' | 'right', boolean> = {
top: false,
bottom: false,
left: false,
right: false,
};
let deltaWidth = 0;
let deltaHeight = 0;

switch (true) {
case isVertical && direction === 'up':
handles.top = true;
deltaHeight = step;
break;
case isVertical && direction === 'down':
handles.bottom = true;
deltaHeight = step;
break;
case !isVertical && direction === 'left':
handles[isRTL ? 'right' : 'left'] = true;
deltaWidth = step;
break;
case !isVertical && direction === 'right':
handles[isRTL ? 'left' : 'right'] = true;
deltaWidth = step;
break;
default:
return;
}

const currentRect = getBoundingRect($appointment[0]);

if (!this._initialSize) {
this._initialSize = { width: currentRect.width, height: currentRect.height };
this._initialCoordinates = locate($appointment);
this.resizeOccur = true;
this._$currentAppointment = $appointment;
}

const initialWidth = this._initialSize.width;
const initialHeight = this._initialSize.height;

const newWidth = initialWidth + deltaWidth;
const newHeight = initialHeight + deltaHeight;

const resizeEvent = {
element: $appointment[0],
handles,
width: newWidth,
height: newHeight,
};

this._resizeEndHandler(resizeEvent, ($updatedElement) => {
if ($updatedElement && $updatedElement.length) {
setActiveAppointment.call(this, $updatedElement[0]);
}
});
};

const arrowKeyHandler = function (e: KeyboardEvent, direction: 'up' | 'down' | 'left' | 'right') {
if (e.shiftKey) {
resizeHandler.call(this, e, direction);
return;
}

const navigatableItems = this._getNavigatableItems();
const focusedItem = navigatableItems.filter('.dx-state-focused');

if (!focusedItem.length) {
return;
}

interface Bounds {
left: number;
right: number;
top: number;
bottom: number;
centerX: number;
centerY: number;
}

const createBounds = (rect: DOMRect): Bounds => ({
left: rect.left,
right: rect.left + rect.width,
top: rect.top,
bottom: rect.top + rect.height,
centerX: rect.left + rect.width / 2,
centerY: rect.top + rect.height / 2,
});

const calculateOverlap = (focusedBounds: Bounds, itemBounds: Bounds, axis: 'horizontal' | 'vertical'): number => {
if (axis === 'horizontal') {
return Math.max(0, Math.min(focusedBounds.right, itemBounds.right) - Math.max(focusedBounds.left, itemBounds.left));
}
return Math.max(0, Math.min(focusedBounds.bottom, itemBounds.bottom) - Math.max(focusedBounds.top, itemBounds.top));
};

const calculatePerpendicularDistance = (focusedBounds: Bounds, itemBounds: Bounds, axis: 'horizontal' | 'vertical'): number => (
axis === 'horizontal'
? Math.abs(focusedBounds.centerY - itemBounds.centerY)
: Math.abs(focusedBounds.centerX - itemBounds.centerX)
);

const focusedBounds = createBounds(getBoundingRect(focusedItem[0]));

const directionConfig: Record<'up' | 'down' | 'left' | 'right', {
isValid: (item: Bounds) => boolean;
getDistance: (item: Bounds) => number;
overlapAxis: 'horizontal' | 'vertical';
}> = {
up: {
isValid: (item: Bounds) => item.bottom <= focusedBounds.top,
getDistance: (item: Bounds) => focusedBounds.top - item.bottom,
overlapAxis: 'horizontal',
},
down: {
isValid: (item: Bounds) => item.top >= focusedBounds.bottom,
getDistance: (item: Bounds) => item.top - focusedBounds.bottom,
overlapAxis: 'horizontal',
},
left: {
isValid: (item: Bounds) => item.right <= focusedBounds.left,
getDistance: (item: Bounds) => focusedBounds.left - item.right,
overlapAxis: 'vertical',
},
right: {
isValid: (item: Bounds) => item.left >= focusedBounds.right,
getDistance: (item: Bounds) => item.left - focusedBounds.right,
overlapAxis: 'vertical',
},
};

const config = directionConfig[direction];

let bestCandidate: dxElementWrapper | null = null;
let bestDistance = Infinity;

navigatableItems.each((_, item) => {
const $item = $(item);
if ($item.is(focusedItem)) {
return;
}

const itemBounds = createBounds(getBoundingRect(item));

if (!config.isValid(itemBounds)) {
return;
}

const overlap = calculateOverlap(focusedBounds, itemBounds, config.overlapAxis);
let distance = config.getDistance(itemBounds);

if (overlap === 0) {
distance += calculatePerpendicularDistance(focusedBounds, itemBounds, config.overlapAxis);
}

if (distance < bestDistance) {
bestDistance = distance;
bestCandidate = $item;
}
});

if (bestCandidate) {
e.preventDefault();
e.stopPropagation();
setActiveAppointment.call(this, bestCandidate);
}
};

const homeEndHandler = function (e: KeyboardEvent, goToFirst: boolean) {
const navigatableItems = this._getNavigatableItems();
if (!navigatableItems.length) {
return;
}

const $targetAppointment = goToFirst
? navigatableItems.first()
: navigatableItems.last();

if ($targetAppointment.length) {
e.preventDefault();
e.stopPropagation();
setActiveAppointment.call(this, $targetAppointment);
}
};

Expand All @@ -176,6 +398,34 @@ class SchedulerAppointments extends CollectionWidget {
}
}.bind(this),
tab: tabHandler,
upArrow: function (e) {
arrowKeyHandler.call(this, e, 'up');
}.bind(this),
downArrow: function (e) {
arrowKeyHandler.call(this, e, 'down');
}.bind(this),
leftArrow: function (e) {
arrowKeyHandler.call(this, e, 'left');
}.bind(this),
rightArrow: function (e) {
arrowKeyHandler.call(this, e, 'right');
}.bind(this),
home: function (e: KeyboardEvent) {
if (!e.ctrlKey) {
homeEndHandler.call(this, e, true);
}
}.bind(this),
end: function (e: KeyboardEvent) {
if (!e.ctrlKey) {
homeEndHandler.call(this, e, false);
}
}.bind(this),
ctrlHome: function (e: KeyboardEvent) {
homeEndHandler.call(this, e, true);
}.bind(this),
ctrlEnd: function (e: KeyboardEvent) {
homeEndHandler.call(this, e, false);
}.bind(this),
});
}

Expand Down Expand Up @@ -767,7 +1017,7 @@ class SchedulerAppointments extends CollectionWidget {
}) || area;
}

_resizeEndHandler(e) {
_resizeEndHandler(e, onUpdated?: ($updatedElement: dxElementWrapper) => void) {
const $element = $(e.element);

const { allDay, info } = $element.data(APPOINTMENT_SETTINGS_KEY) as any;
Expand All @@ -793,6 +1043,11 @@ class SchedulerAppointments extends CollectionWidget {
dateRange,
this.dataAccessors,
this.option('timeZoneCalculator'),
($updatedElement) => {
if (onUpdated) {
onUpdated($updatedElement);
}
},
);
}

Expand Down Expand Up @@ -823,6 +1078,7 @@ class SchedulerAppointments extends CollectionWidget {
dateRange: { startDate: Date; endDate: Date },
dataAccessors: AppointmentDataAccessor,
timeZoneCalculator,
onUpdated?: ($updatedElement: dxElementWrapper) => void,
) {
const sourceAppointment = (this as any)._getItemData($element);
const gridAdapter = new AppointmentAdapter(
Expand Down Expand Up @@ -857,6 +1113,7 @@ class SchedulerAppointments extends CollectionWidget {
target: sourceAppointment,
data,
$appointment: $element,
onUpdated,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,7 @@ class Scheduler extends SchedulerOptionsBaseWidget {
this._a11yStatus = createA11yStatusContainer();
this._a11yStatus.prependTo(this.$element());
// @ts-expect-error
this.setAria({ role: 'group' });
this.setAria({ role: 'application' });
}

_initMarkupOnResourceLoaded() {
Expand Down
9 changes: 7 additions & 2 deletions packages/devextreme/js/__internal/scheduler/m_subscribes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ const subscribes = {
updateAppointmentAfterResize(options) {
const { info } = utils.dataAccessors.getAppointmentSettings(options.$appointment) as AppointmentItemViewModel;
const { startDate } = info.sourceAppointment;
const { onUpdated, target, data } = options;

this._checkRecurringAppointment(options.target, options.data, startDate, () => {
this._updateAppointment(options.target, options.data, function () {
this._checkRecurringAppointment(target, data, startDate, () => {
this._updateAppointment(target, data, function () {
this._appointments.moveAppointmentBack();
}).always((storeAppointment) => {
if (onUpdated && storeAppointment) {
onUpdated(this._appointments._findItemElementByItem(storeAppointment));
}
});
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2473,7 +2473,8 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {

(this.$element() as any)
.addClass(COMPONENT_CLASS)
.addClass(this._getElementClass());
.addClass(this._getElementClass())
.attr('role', 'grid');
}

_initPositionHelper() {
Expand Down
Loading