diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (fluent.blue.light).png new file mode 100644 index 000000000000..964740350f08 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (generic.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (generic.light).png new file mode 100644 index 000000000000..128a2ad9a1f1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (generic.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (material.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (material.blue.light).png new file mode 100644 index 000000000000..aff6dd4b7abc Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/etalons/collector-tooltip-focused-list-item (material.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/tooltipBehavior.ts b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/tooltipBehavior.ts index 3f127c769c61..df03bf02c881 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/tooltipBehavior.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/tooltipBehaviour/tooltipBehavior.ts @@ -6,6 +6,7 @@ import dataSource from './init/widget.data'; import { createScheduler, scroll } from './init/widget.setup'; import url from '../../../../helpers/getPageUrl'; import { DEFAULT_BROWSER_SIZE } from '../../../../helpers/const'; +import { Themes } from '../../../../helpers/themes'; import { testScreenshot } from '../../../../helpers/themeUtils'; fixture.disablePageReloads`Appointment tooltip behavior during scrolling in the Scheduler (T755449)` @@ -125,6 +126,55 @@ test.meta({ browserSize: [600, 400] })('The tooltip should hide after manually s })); }); +test.meta({ + browserSize: DEFAULT_BROWSER_SIZE, + themes: [Themes.fluentBlue, Themes.genericLight, Themes.materialBlue], +})('Collector tooltip focused list item screenshot', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const scheduler = new Scheduler('#container'); + const collector = scheduler.collectors.find('2 more'); + const { appointmentTooltip } = scheduler; + + await t + .expect(collector.element.exists) + .ok() + .click(collector.element) + .expect(appointmentTooltip.isVisible()) + .ok() + .pressKey('tab'); + + await testScreenshot( + t, + takeScreenshot, + 'collector-tooltip-focused-list-item.png', + { element: scheduler.element }, + ); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2017, 4, 22, 9, 30, 0, 0), + endDate: new Date(2017, 4, 22, 10, 30, 0, 0), + }, { + text: 'Text2', + startDate: new Date(2017, 4, 22, 9, 30, 0, 0), + endDate: new Date(2017, 4, 22, 10, 30, 0, 0), + }, { + text: 'Text3', + startDate: new Date(2017, 4, 22, 9, 30, 0, 0), + endDate: new Date(2017, 4, 22, 10, 30, 0, 0), + }], + views: [{ + type: 'month', + maxAppointmentsPerCell: 1, + }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), +})); + test.meta({ browserSize: [600, 1000] })('Tooltip on mobile devices should have enough hight if there are async templates (React)', async (t) => { const { takeScreenshot, compareResults } = createScreenshotsComparer(t); const scheduler = new Scheduler('#container'); diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss index 24b04b1d6359..309239cf053d 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss @@ -22,6 +22,7 @@ $scheduler-fill-focused-appointment: null !default; $scheduler-is-shadow-color-for-focused-state: null !default; $scheduler-dd-appointment-hover-text-color: null !default; $scheduler-tooltip-appointment-text-color: null !default; +$scheduler-tooltip-list-focused-bg: null !default; $scheduler-timeline-cell-height: null !default; $scheduler-appointment-overlay-bg: null !default; @@ -47,7 +48,8 @@ $agenda-appointment-recurrence-icon-color: null !default; @use "./tooltip" with ( $scheduler-appointment-base-color: $scheduler-appointment-base-color, $scheduler-tooltip-appointment-text-color: $scheduler-tooltip-appointment-text-color, - $scheduler-appointment-overlay-bg: $scheduler-appointment-overlay-bg + $scheduler-appointment-overlay-bg: $scheduler-appointment-overlay-bg, + $scheduler-tooltip-list-focused-bg: $scheduler-tooltip-list-focused-bg ); $scheduler-appointment-min-size: 4px; diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/_tooltip.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/_tooltip.scss index 1e8c59f2fc01..ef43001ff74c 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/_tooltip.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/_tooltip.scss @@ -3,6 +3,7 @@ $scheduler-appointment-base-color: null !default; $scheduler-tooltip-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: null !default; +$scheduler-tooltip-list-focused-bg: null !default; .dx-popup-wrapper.dx-scheduler-appointment-tooltip-wrapper, .dx-scheduler-overlay-panel { @@ -15,6 +16,10 @@ $scheduler-appointment-overlay-bg: null !default; .dx-list-item-content { padding: 5px; } + + &.dx-state-focused { + background-color: $scheduler-tooltip-list-focused-bg; + } } } diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss index 78d3cee8c9c2..1e86184428c2 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss @@ -4,6 +4,7 @@ @use "../colors" as *; @use "sizes" as *; @use "../sizes" as *; +@use "../list/colors" as *; @use "../form/sizes" as *; @use "../radioGroup/sizes" as *; @use "../toolbar/sizes" as *; @@ -13,6 +14,7 @@ @use "../../base/scheduler" as baseScheduler with ( $scheduler-tooltip-appointment-text-color: $scheduler-tooltip-appointment-text-color, $scheduler-appointment-overlay-bg: $scheduler-appointment-overlay-bg, + $scheduler-tooltip-list-focused-bg: $list-item-hover-bg, $scheduler-appointment-base-color: $scheduler-appointment-base-color, $scheduler-appointment-start-color: $scheduler-appointment-start-color, $scheduler-appointment-active-color: $scheduler-appointment-active-color, diff --git a/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss index 4bef8ff8be1f..16f816c52700 100644 --- a/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss @@ -4,6 +4,7 @@ @use "../colors" as *; @use "sizes" as *; @use "../sizes" as *; +@use "../list/colors" as *; @use "../toolbar/sizes" as *; @use '../../base/scheduler/layout/header' as *; @use "../../base/mixins" as *; @@ -11,6 +12,7 @@ @use "../../base/scheduler" as baseScheduler with ( $scheduler-tooltip-appointment-text-color: $scheduler-tooltip-appointment-text-color, $scheduler-appointment-overlay-bg: $scheduler-appointment-overlay-bg, + $scheduler-tooltip-list-focused-bg: $list-item-hover-bg, $scheduler-appointment-base-color: $scheduler-appointment-base-color, $scheduler-appointment-start-color: $scheduler-appointment-start-color, $scheduler-base-border-color: $scheduler-base-border-color, diff --git a/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss index 073b4638f901..c7974b5d62bc 100644 --- a/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss @@ -4,6 +4,7 @@ @use "../colors" as *; @use "sizes" as *; @use "../sizes" as *; +@use "../list/colors" as *; @use "../radioGroup/sizes" as *; @use "../toolbar/sizes" as *; @use '../../base/scheduler/layout/header' as *; @@ -12,6 +13,7 @@ @use "../../base/scheduler" as baseScheduler with ( $scheduler-tooltip-appointment-text-color: $scheduler-tooltip-appointment-text-color, $scheduler-appointment-overlay-bg: $scheduler-appointment-overlay-bg, + $scheduler-tooltip-list-focused-bg: $list-item-hover-bg, $scheduler-appointment-base-color: $scheduler-appointment-base-color, $scheduler-appointment-start-color: $scheduler-appointment-start-color, $scheduler-base-border-color: $scheduler-base-border-color, diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts index 7ef97ce50136..486c70da7ea1 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -6,6 +6,7 @@ import { POPUP_DIALOG_CLASS } from '../../../m_scheduler'; import type { AppointmentModel } from './appointment'; import { createAppointmentModel } from './appointment'; import { PopupModel } from './popup'; +import { TooltipModel } from './tooltip'; const getTexts = ( cells: ArrayLike, @@ -25,6 +26,10 @@ export class SchedulerModel { return this.getPopup(); } + get tooltip(): TooltipModel { + return new TooltipModel(); + } + get toolbar(): ToolbarModel { return new ToolbarModel(this.queries.getByRole('toolbar')); } @@ -55,6 +60,17 @@ export class SchedulerModel { return getTexts(collectors); } + getCollectorButton(index = 0): HTMLElement { + const allButtons = this.queries.queryAllByRole('button') as HTMLElement[]; + const collectors = allButtons.filter((btn) => btn.classList.contains('dx-scheduler-appointment-collector')); + + if (collectors.length === 0) { + throw new Error('Collector button not found'); + } + + return collectors[index]; + } + getDateTableContent(): string[] { const cells = this.container.querySelectorAll('.dx-scheduler-date-table-cell'); return getTexts(cells); @@ -120,8 +136,6 @@ export class SchedulerModel { getLoadPanel = (): HTMLElement | null => document.querySelector('.dx-loadpanel'); - getTooltipAppointment = (): HTMLElement | null => document.querySelector('.dx-tooltip-appointment-item'); - openPopupByDblClick(text?: string): AppointmentModel { const appointment = this.getAppointment(text) as AppointmentModel; diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/tooltip.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/tooltip.ts new file mode 100644 index 000000000000..6444af4f4e57 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/tooltip.ts @@ -0,0 +1,38 @@ +import { within } from '@testing-library/dom'; + +const TOOLTIP_WRAPPER_SELECTOR = '.dx-overlay-wrapper.dx-scheduler-appointment-tooltip-wrapper'; + +export class TooltipModel { + private get element(): HTMLElement | null { + return document.querySelector(TOOLTIP_WRAPPER_SELECTOR); + } + + isVisible(): boolean { + return this.element !== null; + } + + getScrollableContent(): Element | null { + return this.element?.querySelector('.dx-scrollable .dx-scrollview-content') ?? null; + } + + getDeleteButton(index = 0): HTMLElement { + const tooltip = this.element; + const buttons = tooltip + ? within(tooltip).queryAllByRole('button').filter((btn) => btn.classList.contains('dx-tooltip-appointment-item-delete-button')) + : []; + + if (buttons.length === 0) { + throw new Error('Tooltip delete button not found'); + } + + return buttons[index]; + } + + getAppointmentItem(index = 0): HTMLElement | null { + const tooltip = this.element; + if (!tooltip) { + return null; + } + return within(tooltip).queryAllByRole('option')[index] ?? null; + } +} diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts new file mode 100644 index 000000000000..cfc59690a43c --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts @@ -0,0 +1,162 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import fx from '@js/common/core/animation/fx'; + +import { createScheduler } from './__mock__/create_scheduler'; +import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; + +describe('Appointment tooltip behavior', () => { + beforeEach(() => { + fx.off = true; + setupSchedulerTestEnvironment(); + }); + + afterEach(() => { + fx.off = false; + jest.useRealTimers(); + document.body.innerHTML = ''; + }); + + it('should delete appointment by Delete key when focused in tooltip from collector', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + ], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + }); + + POM.getCollectorButton().click(); + + const tooltipScrollableContent = POM.tooltip.getScrollableContent(); + tooltipScrollableContent?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + tooltipScrollableContent?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); + + expect(POM.tooltip.isVisible()).toBe(false); + }); + + it('should delete appointment on delete button click in tooltip', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + ], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + }); + + POM.getCollectorButton().click(); + POM.tooltip.getDeleteButton().click(); + + expect(POM.tooltip.isVisible()).toBe(false); + }); + + it('delete button in tooltip should not be focusable using tab', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + ], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + }); + + POM.getCollectorButton().click(); + + expect(POM.tooltip.getDeleteButton().getAttribute('tabindex')).toBe('-1'); + }); + + it('should not delete appointment by Delete key when editing.allowDeleting=false', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + ], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + editing: { + allowDeleting: false, + }, + }); + + POM.getCollectorButton().click(); + + const tooltipScrollableContent = POM.tooltip.getScrollableContent(); + tooltipScrollableContent?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + tooltipScrollableContent?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); + + expect(POM.tooltip.isVisible()).toBe(true); + }); + + it('should not delete disabled appointment by Delete key when focused in tooltip from collector', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + disabled: true, + }, + ], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + }); + + POM.getCollectorButton().click(); + + const tooltipScrollableContent = POM.tooltip.getScrollableContent(); + tooltipScrollableContent?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + tooltipScrollableContent?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); + + expect(POM.tooltip.isVisible()).toBe(true); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts index b02f955123f1..0b3ff60eecd0 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts @@ -82,7 +82,7 @@ describe('Appointments', () => { appointment.element.click(); jest.advanceTimersByTime(1000); - const tooltipAppointment = POM.getTooltipAppointment(); + const tooltipAppointment = POM.tooltip.getAppointmentItem(); expect(tooltipAppointment).not.toBeNull(); const tooltipTitleElement = tooltipAppointment?.querySelector('.dx-tooltip-appointment-item-content-subject'); diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts index 1cac7e00ffe9..557fddc63fe1 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts @@ -2241,7 +2241,7 @@ describe('Appointment Popup', () => { POM.getAppointment('common-app').element?.click(); jest.runAllTimers(); - POM.getTooltipAppointment()?.click(); + POM.tooltip.getAppointmentItem()?.click(); expect(POM.isPopupVisible()).toBe(true); expect(POM.popup.dxForm.option('formData')).toMatchObject({ ...commonAppointment }); diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy.ts index e9f44a6022e9..fa181c0b6140 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy.ts @@ -8,22 +8,22 @@ const APPOINTMENT_TOOLTIP_WRAPPER_CLASS = 'dx-scheduler-appointment-tooltip-wrap const MAX_TOOLTIP_HEIGHT = 200; export class DesktopTooltipStrategy extends TooltipStrategyBase { - _prepareBeforeVisibleChanged(dataList) { + protected override prepareBeforeVisibleChanged(dataList) { this._tooltip.option('position', { my: 'bottom', at: 'top', - boundary: this._getBoundary(dataList), + boundary: this.getBoundary(dataList), offset: this._extraOptions.offset, collision: 'fit flipfit', }); } - _getBoundary(dataList) { + private getBoundary(dataList) { return this._options.isAppointmentInAllDayPanel(dataList[0].appointment) ? this._options.container : this._options.getScrollableContainer(); } - _onShown() { - super._onShown(); + protected override onShown() { + super.onShown(); if (this._extraOptions.isButtonClick) { this._list.focus(); this._list.option('focusedElement', null); @@ -31,24 +31,24 @@ export class DesktopTooltipStrategy extends TooltipStrategyBase { } // @ts-expect-error - _createListOption(target, dataList) { + protected override createListOption(target, dataList) { // @ts-expect-error - const result: any = super._createListOption(target, dataList); + const result: any = super.createListOption(target, dataList); // T724287 this condition is not covered by tests, because touch variable cannot be overridden. // In the future, it is necessary to cover the tests result.showScrollbar = supportUtils.touch ? 'always' : 'onHover'; return result; } - _createTooltip(target, dataList) { - const tooltipElement = this._createTooltipElement(APPOINTMENT_TOOLTIP_WRAPPER_CLASS); + protected override createTooltip(target, dataList) { + const tooltipElement = this.createTooltipElement(APPOINTMENT_TOOLTIP_WRAPPER_CLASS); const tooltip = this._options.createComponent(tooltipElement, Tooltip, { target, maxHeight: MAX_TOOLTIP_HEIGHT, rtlEnabled: this._extraOptions.rtlEnabled, - onShown: this._onShown.bind(this), - contentTemplate: this._getContentTemplate(dataList), + onShown: this.onShown.bind(this), + contentTemplate: this.getContentTemplate(dataList), wrapperAttr: { class: APPOINTMENT_TOOLTIP_WRAPPER_CLASS }, _loopFocus: this._extraOptions._loopFocus, }); @@ -61,11 +61,11 @@ export class DesktopTooltipStrategy extends TooltipStrategyBase { return tooltip; } - _onListRender(e) { + protected override onListRender(e) { return this._extraOptions.dragBehavior && this._extraOptions.dragBehavior(e); } - _onListItemContextMenu(e) { + protected override onListItemContextMenu(e) { const contextMenuEventArgs = this._options.createEventArgs(e); this._options.onItemContextMenu(contextMenuEventArgs); } diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_mobile_tooltip_strategy.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_mobile_tooltip_strategy.ts index b88b1d8ec06e..69668b54f68e 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_mobile_tooltip_strategy.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_mobile_tooltip_strategy.ts @@ -64,7 +64,7 @@ const createTabletDeviceConfig = (listHeight) => { }; export class MobileTooltipStrategy extends TooltipStrategyBase { - _shouldUseTarget() { + protected override shouldUseTarget() { return false; } @@ -91,8 +91,8 @@ export class MobileTooltipStrategy extends TooltipStrategyBase { this.setTooltipConfig(); } - _createTooltip(target, dataList) { - const element = this._createTooltipElement(CLASS.slidePanel); + protected override createTooltip(target, dataList) { + const element = this.createTooltipElement(CLASS.slidePanel); return this._options.createComponent(element, Overlay, { target: getWindow(), @@ -100,8 +100,8 @@ export class MobileTooltipStrategy extends TooltipStrategyBase { animation: animationConfig, onShowing: () => this._onShowing(), - onShown: this._onShown.bind(this), - contentTemplate: this._getContentTemplate(dataList), + onShown: this.onShown.bind(this), + contentTemplate: this.getContentTemplate(dataList), wrapperAttr: { class: CLASS.slidePanel }, }); } diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts index f38629086d3f..c50a397c14df 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts @@ -35,43 +35,66 @@ export class TooltipStrategyBase { } show(target, dataList, extraOptions) { - if (this._canShowTooltip(dataList)) { + if (this.canShowTooltip(dataList)) { this.hide(); this._extraOptions = extraOptions; - this._showCore(target, dataList); + this.showCore(target, dataList); } } - _showCore(target, dataList) { + private showCore(target, dataList) { const describedByValue = isRenderer(target) && target.attr('aria-describedby') as string; if (!this._tooltip) { - this._tooltip = this._createTooltip(target, dataList); + this._tooltip = this.createTooltip(target, dataList); } else { - this._shouldUseTarget() && this._tooltip.option('target', target); + this.shouldUseTarget() && this._tooltip.option('target', target); this._list.option('dataSource', dataList); } - this._prepareBeforeVisibleChanged(dataList); + this.prepareBeforeVisibleChanged(dataList); this._tooltip.option('visible', true); describedByValue && target.attr('aria-describedby', describedByValue); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _prepareBeforeVisibleChanged(dataList) { + protected prepareBeforeVisibleChanged(dataList) { } - _getContentTemplate(dataList) { + private isDeletingAllowed(appointment) { + const { editing } = this._extraOptions; + const disabled = this._options.getAppointmentDisabled(appointment); + const isDeletingAllowed = editing === true || editing?.allowDeleting === true; + return !disabled && isDeletingAllowed; + } + + protected getContentTemplate(dataList) { return (container) => { const listElement = $('
'); $(container).append(listElement); - this._list = this._createList(listElement, dataList); + this._list = this.createList(listElement, dataList); this._list.registerKeyHandler?.('escape', () => { this.hide(); this._tooltip.option('target').focus(); }); + this._list.registerKeyHandler?.('del', () => { + const { focusedElement } = this._list.option(); + if (!focusedElement) { + return; + } + + const { appointment, targetedAppointment } = this._list._getItemData(focusedElement); + if (!appointment) { + return; + } + + if (this.isDeletingAllowed(appointment)) { + this.hide(); + this._options.checkAndDeleteAppointment(appointment, targetedAppointment); + } + }); }; } @@ -82,7 +105,7 @@ export class TooltipStrategyBase { return undefined; } - _onShown() { + protected onShown() { this._list.option('focusStateEnabled', this._extraOptions.focusStateEnabled); } @@ -95,46 +118,46 @@ export class TooltipStrategyBase { } } - _shouldUseTarget() { + protected shouldUseTarget() { return true; } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _createTooltip(target, dataList) { + protected createTooltip(target, dataList) { } - _canShowTooltip(dataList) { + private canShowTooltip(dataList) { if (!dataList.length) { return false; } return true; } - _createListOption(dataList) { + protected createListOption(dataList) { return { dataSource: dataList, - onContentReady: this._onListRender.bind(this), - onItemClick: (e) => this._onListItemClick(e), - onItemContextMenu: this._onListItemContextMenu.bind(this), - itemTemplate: (item, index) => this._renderTemplate(item.appointment, item.targetedAppointment, index, item.color), + onContentReady: this.onListRender.bind(this), + onItemClick: (e) => this.onListItemClick(e), + onItemContextMenu: this.onListItemContextMenu.bind(this), + itemTemplate: (item, index) => this.renderTemplate(item.appointment, item.targetedAppointment, index, item.color), _swipeEnabled: false, pageLoadMode: 'scrollBottom', }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _onListRender(e) {} + protected onListRender(e) { } - _createTooltipElement(wrapperClass) { + protected createTooltipElement(wrapperClass) { return $('
').appendTo(this._options.container).addClass(wrapperClass); } - _createList(listElement, dataList) { - return this._options.createComponent(listElement, List, this._createListOption(dataList)); + private createList(listElement, dataList) { + return this._options.createComponent(listElement, List, this.createListOption(dataList)); } - _renderTemplate(appointment, targetedAppointment, index, color) { - const itemListContent = this._createItemListContent(appointment, targetedAppointment, color); + private renderTemplate(appointment, targetedAppointment, index, color) { + const itemListContent = this.createItemListContent(appointment, targetedAppointment, color); this._options.addDefaultTemplates({ // @ts-expect-error appointmentTooltip: new FunctionTemplate((options) => { @@ -145,10 +168,10 @@ export class TooltipStrategyBase { }); const template = this._options.getAppointmentTemplate(APPOINTMENT_TOOLTIP_TEMPLATE); - return this._createFunctionTemplate(template, appointment, targetedAppointment, index); + return this.createFunctionTemplate(template, appointment, targetedAppointment, index); } - _createFunctionTemplate(template, appointmentData, targetedAppointmentData, index) { + private createFunctionTemplate(template, appointmentData, targetedAppointmentData, index) { const isButtonClicked = Boolean(this._extraOptions.isButtonClick); // @ts-expect-error @@ -172,31 +195,28 @@ export class TooltipStrategyBase { }); } - _onListItemClick(e) { + private onListItemClick(e) { this.hide(); this._extraOptions.clickEvent && this._extraOptions.clickEvent(e); this._options.showAppointmentPopup(e.itemData.appointment, false, e.itemData.targetedAppointment); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _onListItemContextMenu(e) {} + protected onListItemContextMenu(e) { } - _createItemListContent(appointment, targetedAppointment, color) { - const { editing } = this._extraOptions; + private createItemListContent(appointment, targetedAppointment, color) { const $itemElement = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM); - $itemElement.append(this._createItemListMarker(color)); - $itemElement.append(this._createItemListInfo(this._options.createFormattedDateText(appointment, targetedAppointment))); - - const disabled = this._options.getAppointmentDisabled(appointment); + $itemElement.append(this.createItemListMarker(color)); + $itemElement.append(this.createItemListInfo(this._options.createFormattedDateText(appointment, targetedAppointment))); - if (!disabled && (editing && editing.allowDeleting === true || editing === true)) { - $itemElement.append(this._createDeleteButton(appointment, targetedAppointment)); + if (this.isDeletingAllowed(appointment)) { + $itemElement.append(this.createDeleteButton(appointment, targetedAppointment)); } return $itemElement; } - _createItemListMarker(color) { + private createItemListMarker(color) { const $marker = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_MARKER); const $markerBody = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_MARKER_BODY); @@ -210,7 +230,7 @@ export class TooltipStrategyBase { return $marker; } - _createItemListInfo(object) { + private createItemListInfo(object) { const result = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT); const $title = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT_SUBJECT).text(object.text); const $date = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT_DATE).text(object.formatDate); @@ -218,7 +238,7 @@ export class TooltipStrategyBase { return result.append($title).append($date); } - _createDeleteButton(appointment, targetedAppointment) { + private createDeleteButton(appointment, targetedAppointment) { const $container = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON_CONTAINER); const $deleteButton = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON); @@ -226,6 +246,7 @@ export class TooltipStrategyBase { this._options.createComponent($deleteButton, Button, { icon: 'trash', stylingMode: 'text', + tabIndex: -1, onClick: (e) => { this.hide(); e.event.stopPropagation();