diff --git a/apps/react-storybook/stories/card_view/CardView.stories.tsx b/apps/react-storybook/stories/card_view/CardView.stories.tsx
index 904e1f029167..7bd2f630a602 100644
--- a/apps/react-storybook/stories/card_view/CardView.stories.tsx
+++ b/apps/react-storybook/stories/card_view/CardView.stories.tsx
@@ -298,3 +298,32 @@ export const SelectionStory: Story = {
}
}
+export const ContextMenuStory: Story = {
+ ...DefaultMode,
+ args: {
+ ...DefaultMode.args,
+ onContextMenuPreparing: (e) => {
+ e.items = e.items ?? [];
+
+ if(e.target === 'toolbar') {
+ e.items.push({
+ text: 'show column chooser',
+ onItemClick: () => e.component.showColumnChooser()
+ });
+ }
+ else if(e.target === 'headerPanel' && e.column) {
+ e.items.push({
+ text: `hide ${e.column.caption}`,
+ disabled: !e.column.visible,
+ icon: 'eyeclose',
+ onItemClick: () => e.component.columnOption(e.columnIndex, 'visible', false)
+ });
+ }
+ else if(e.target === 'content' && e.card) {
+ e.items.push({
+ text: 'do something with card'
+ });
+ }
+ }
+ }
+}
diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts
index 58ff9e9158d0..b2d36dd7ea7b 100644
--- a/packages/devextreme-themebuilder/tests/data/dependencies.ts
+++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts
@@ -16,7 +16,7 @@ export const dependencies: FlatStylesDependencies = {
buttongroup: ['validation', 'button'],
dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'],
calendar: ['validation', 'button'],
- cardview: ['box', 'button', 'calendar', 'checkbox', 'datebox', 'filterbuilder', 'list', 'loadindicator', 'loadpanel', 'numberbox', 'popup', 'scrollview', 'selectbox', 'sortable', 'textbox', 'toast', 'toolbar', 'treeview', 'validation'],
+ cardview: ['box', 'button', 'calendar', 'checkbox', 'contextmenu', 'datebox', 'filterbuilder', 'list', 'loadindicator', 'loadpanel', 'numberbox', 'popup', 'scrollview', 'selectbox', 'sortable', 'textbox', 'toast', 'toolbar', 'treeview', 'validation'],
chat: ['button', 'loadindicator', 'loadpanel', 'scrollview', 'textbox', 'validation'],
checkbox: ['validation'],
numberbox: ['validation', 'button', 'loadindicator'],
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap
index c684f0dd7cdb..a7dabd4eb47c 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap
+++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap
@@ -182,5 +182,13 @@ exports[`common initial render should be successfull 1`] = `
+
`;
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx
index 34b08c80cc59..60937c9d1501 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx
@@ -72,6 +72,8 @@ export interface CardProps {
onPrepared?: (e: CardPreparedEvent) => void;
+ onContextMenu?: (e: MouseEvent, card?: DataRow, cardIndex?: number) => void;
+
selectCard?: (row: DataRow, options: SelectCardOptions) => void;
}
@@ -111,6 +113,7 @@ export class Card extends Component {
onDblClick={this.handleDoubleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
+ onContextMenu={this.props.onContextMenu}
>
void;
+ showContextMenu?: (e: MouseEvent, card?: DataRow, cardIndex?: number) => void;
+
cardsPerRow?: number;
needToHiddenCheckBoxes?: boolean;
@@ -88,14 +90,18 @@ export class Content extends Component {
className={className}
style={this.getCssVariables()}
ref={this.containerRef}
+ onContextMenu={this.props.showContextMenu}
>
- {this.props.items.map((item, i) => (
+ {this.props.items.map((item, index) => (
{
+ this.props.showContextMenu?.(e, item, index);
+ }}
/>
))}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx
index cec5c649603a..ad6fc036c914 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { ContentViewProps as ContentViewBaseProps } from '@ts/grids/new/grid_core/content_view/content_view';
import { ContentView as ContentViewBase } from '@ts/grids/new/grid_core/content_view/content_view';
import type { InfernoNode } from 'inferno';
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx
index b4bec55041ef..8b6292754ae0 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx
@@ -49,6 +49,7 @@ export class ContentView extends ContentViewBase {
fieldTemplate: this.options.template('fieldTemplate'),
cardsPerRow: this.cardsPerRow,
onRowHeightChange: this.rowHeight.update.bind(this.rowHeight),
+ showContextMenu: this.showContextMenu.bind(this),
cardProps: combined({
minWidth: this.cardMinWidth,
maxWidth: this.options.oneWay('cardMaxWidth'),
@@ -100,4 +101,8 @@ export class ContentView extends ContentViewBase {
private onCardHold(e: CardHoldEvent) {
this.selectionController.processLongTap(e.row);
}
+
+ private showContextMenu(e: MouseEvent, card?: DataRow, cardIndex?: number): void {
+ this.contextMenuController.show(e, 'content', { card, cardIndex });
+ }
}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.mock.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.mock.ts
new file mode 100644
index 000000000000..f35a967157d4
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.mock.ts
@@ -0,0 +1,14 @@
+import type { Item as ContextMenuItem } from '@js/ui/context_menu';
+
+import type { ContextInfo, ContextMenuTarget } from '.';
+import { ContextMenuController } from '.';
+
+export class ContextMenuControllerMock extends ContextMenuController {
+ public override getItems(
+ view: ContextMenuTarget,
+ targetElement: Element,
+ contextInfo: ContextInfo = {},
+ ): ContextMenuItem[] | undefined {
+ return super.getItems(view, targetElement, contextInfo);
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.test.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.test.ts
new file mode 100644
index 000000000000..be88221bf82c
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.test.ts
@@ -0,0 +1,41 @@
+import {
+ describe, expect, it, jest,
+} from '@jest/globals';
+import dxContextMenu from '@js/ui/context_menu';
+
+import { ColumnsController } from '../../grid_core/columns_controller';
+import type { Options } from '../options';
+import { OptionsControllerMock } from '../options_controller.mock';
+import { ContextMenuControllerMock } from './controller.mock';
+
+const setup = (options: Options) => {
+ const optionsController = new OptionsControllerMock(options);
+ const columnsController = new ColumnsController(optionsController);
+ const controller = new ContextMenuControllerMock(columnsController, optionsController);
+
+ const container = document.createElement('div');
+ // eslint-disable-next-line new-cap
+ const contextMenu = new dxContextMenu(container, {
+ onPositioning: controller.onPositioning,
+ });
+
+ // @ts-expect-error
+ controller.contextMenuRef = { current: contextMenu };
+
+ return { controller, contextMenu };
+};
+
+describe('ContextMenu', () => {
+ describe('Controller', () => {
+ it('onContextMenuPreparing is called on getItems()', () => {
+ const onContextMenuPreparing = jest.fn();
+ const { controller } = setup({
+ onContextMenuPreparing,
+ });
+
+ controller.getItems('content', document.createElement('div'));
+
+ expect(onContextMenuPreparing).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.ts
new file mode 100644
index 000000000000..9d0502c4880f
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/controller.ts
@@ -0,0 +1,96 @@
+/* eslint-disable spellcheck/spell-checker */
+import type { Item as ContextMenuItem, ItemClickEvent } from '@js/ui/context_menu';
+
+import { ColumnsController } from '../../grid_core/columns_controller/index';
+import type { Column, DataRow } from '../../grid_core/columns_controller/types';
+import { BaseContextMenuController } from '../../grid_core/context_menu/controller';
+import { OptionsController } from '../options_controller';
+import type { ContextMenuPreparingEvent, ContextMenuTarget } from '.';
+
+export interface ContextInfo {
+ column?: Column;
+ columnIndex?: number;
+ card?: DataRow;
+ cardIndex?: number;
+}
+
+export class ContextMenuController
+ extends BaseContextMenuController {
+ public static dependencies = [ColumnsController, OptionsController] as const;
+
+ constructor(
+ private readonly columnsController: ColumnsController,
+ private readonly options: OptionsController,
+ ) {
+ super();
+ }
+
+ public override show(
+ event: MouseEvent,
+ view: ContextMenuTarget,
+ contextInfo: ContextInfo = {},
+ ): void {
+ super.show(event, view, contextInfo);
+ }
+
+ protected override getItems(
+ view: ContextMenuTarget,
+ targetElement: Element,
+ contextInfo: ContextInfo = {},
+ ): ContextMenuItem[] | undefined {
+ const items: ContextMenuItem[] = [];
+
+ if (view === 'headerPanel' && contextInfo.column) {
+ items.push(...this.getSortingItems(contextInfo.column));
+ }
+
+ // @ts-expect-error
+ const event: ContextMenuPreparingEvent = {
+ items: items.length > 0 ? items : undefined,
+ target: view,
+ targetElement: targetElement as HTMLElement,
+ columnIndex: undefined,
+ card: undefined,
+ cardIndex: undefined,
+ column: undefined,
+
+ ...contextInfo,
+ };
+
+ const callback = this.options.action('onContextMenuPreparing').unreactive_get();
+
+ callback(event);
+
+ return event.items;
+ }
+
+ private getSortingItems(column: Column): ContextMenuItem[] {
+ const onItemClick = (e: ItemClickEvent): void => {
+ this.columnsController.columnOption(column, 'sortOrder', e.itemData?.value);
+ };
+
+ return [
+ {
+ text: this.options.oneWay('sorting.ascendingText').unreactive_get(),
+ value: 'asc',
+ disabled: column.sortOrder === 'asc',
+ icon: 'sortuptext',
+ onItemClick,
+ },
+ {
+ text: this.options.oneWay('sorting.descendingText').unreactive_get(),
+ value: 'desc',
+ disabled: column.sortOrder === 'desc',
+ icon: 'sortdowntext',
+ onItemClick,
+ },
+ {
+ text: this.options.oneWay('sorting.clearText').unreactive_get(),
+ value: undefined,
+ disabled: !column.sortOrder,
+ icon: 'none',
+ onItemClick,
+ },
+ ] as ContextMenuItem[];
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/index.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/index.ts
new file mode 100644
index 000000000000..5026a5a5d22c
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/index.ts
@@ -0,0 +1,3 @@
+export * from './controller';
+export type { ContextMenuPreparingEvent, ContextMenuTarget, Options } from './options';
+export * from './view';
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/options.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/options.ts
new file mode 100644
index 000000000000..10e54bc3d3eb
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/options.ts
@@ -0,0 +1,30 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import type { EventInfo } from '@js/common/core/events';
+import type { DxElement } from '@js/core/element';
+
+import type { Column, DataRow } from '../../grid_core/columns_controller/types';
+
+export type ContextMenuTarget = 'toolbar' | 'headerPanel' | 'content';
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export type ContextMenuPreparingEvent
+= EventInfo & {
+ items?: any[];
+
+ readonly target: ContextMenuTarget;
+
+ readonly targetElement: DxElement;
+
+ readonly columnIndex?: number;
+
+ readonly column?: Column;
+
+ readonly cardIndex?: number;
+
+ readonly card?: TCardData;
+};
+
+export interface Options {
+ onContextMenuPreparing?: (args: ContextMenuPreparingEvent) => void;
+}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.integration.test.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.integration.test.ts
new file mode 100644
index 000000000000..217db8546ae6
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.integration.test.ts
@@ -0,0 +1,274 @@
+import {
+ afterEach, describe, expect, it,
+} from '@jest/globals';
+import type { SortOrder } from '@js/common';
+import $ from '@js/core/renderer';
+import type ContextMenu from '@js/ui/context_menu';
+import type { PositioningEvent } from '@js/ui/context_menu';
+import type { Options as CardViewOptions } from '@ts/grids/new/card_view/options';
+import CardView from '@ts/grids/new/card_view/widget';
+
+import type { ContextMenuPreparingEvent } from '.';
+
+const setup = (options: CardViewOptions = {}): CardView => {
+ const container = document.createElement('div');
+ const { body } = document;
+ body.append(container);
+
+ return new CardView(container, options);
+};
+
+const SELECTORS = {
+ cardView: '.dx-widget.dx-cardview',
+ contextMenu: '.dx-widget.dx-context-menu',
+ contextMenuContent: '.dx-context-menu.dx-overlay-content',
+ toolbar: '.dx-widget.dx-toolbar',
+ headerPanel: '.dx-cardview-headerpanel-content',
+ contentView: '.dx-cardview-content',
+ headerItem: '.dx-cardview-header-item',
+ card: '.dx-cardview-card',
+ menuItem: '.dx-menu-item',
+};
+
+const WIDGET_CONTAINER_CLASS = 'dx-cardview-container';
+
+const rootQuerySelector = (selector: string) => document.body.querySelector(selector);
+
+const getContextMenuInstance = (): ContextMenu => {
+ const contextMenuElement = rootQuerySelector(SELECTORS.contextMenu);
+
+ if (!contextMenuElement) {
+ throw new Error('ContextMenu element not found');
+ }
+
+ // @ts-expect-error
+ const contextMenu = $(contextMenuElement).dxContextMenu('instance') as ContextMenu;
+
+ return contextMenu;
+};
+
+const getContextMenuElement = (): Element => {
+ const contextMenuElement = rootQuerySelector(SELECTORS.contextMenuContent);
+
+ if (!contextMenuElement) {
+ throw new Error('ContextMenu content element not present in the DOM');
+ }
+
+ return contextMenuElement;
+};
+
+const openContextMenu = (cardView: CardView, selector: string): ContextMenuPreparingEvent => {
+ let itemsPreparingEvent: ContextMenuPreparingEvent | null = null;
+
+ cardView.on('contextMenuPreparing', (e) => {
+ itemsPreparingEvent = e;
+ });
+
+ const eventElement = rootQuerySelector(selector);
+
+ const contextMenuEvent = new MouseEvent('contextmenu', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ });
+
+ eventElement?.dispatchEvent(contextMenuEvent);
+
+ if (itemsPreparingEvent === null) {
+ throw new Error('contextMenuPreparing event was not fired');
+ }
+
+ return itemsPreparingEvent;
+};
+
+describe('ContextMenu', () => {
+ describe('View', () => {
+ afterEach(() => {
+ const cardView = rootQuerySelector(SELECTORS.cardView);
+ // @ts-expect-error bad typed renderer
+ $(cardView ?? undefined as any)?.dxCardView('dispose');
+ });
+
+ it('contextMenu.onPositioning event is correct', () => {
+ const cardView = setup({ columns: ['Column 1'] });
+ const contextMenu = getContextMenuInstance();
+
+ expect(contextMenu.option('target')).toBe(undefined);
+ expect(contextMenu.option('showEvent')).toBe(undefined);
+
+ let invokesCount = 0;
+ // eslint-disable-next-line @typescript-eslint/init-declarations
+ let positioningEvent: PositioningEvent | undefined;
+
+ contextMenu.on('positioning', (e: PositioningEvent) => {
+ invokesCount += 1;
+ positioningEvent = e;
+ });
+
+ openContextMenu(cardView, SELECTORS.headerItem);
+
+ expect(invokesCount).toEqual(1);
+ expect(positioningEvent?.event).toBeUndefined();
+ });
+
+ it('contextMenu has class', () => {
+ const cardView = setup({ columns: ['Column 1'] });
+ openContextMenu(cardView, SELECTORS.headerItem);
+
+ const contextMenuElement = getContextMenuElement();
+
+ expect(contextMenuElement.classList).toContain(WIDGET_CONTAINER_CLASS);
+ });
+
+ it.each<{
+ targetView: 'toolbar' | 'headerPanel' | 'content'; selector: string;
+ }>([
+ { targetView: 'toolbar', selector: SELECTORS.toolbar },
+ { targetView: 'headerPanel', selector: SELECTORS.headerPanel },
+ { targetView: 'content', selector: SELECTORS.contentView },
+ ])('onContextMenuPreparing fired on $targetView', ({ targetView, selector }) => {
+ const cardView = setup({ columns: ['Column 1'] });
+ const event = openContextMenu(cardView, selector);
+
+ expect(event.card).toBeUndefined();
+ expect(event.cardIndex).toBeUndefined();
+ expect(event.column).toBeUndefined();
+ expect(event.columnIndex).toBeUndefined();
+
+ expect(event.component).toBe(cardView);
+ expect(event.element).toBe(cardView.element());
+ expect(event.target).toEqual(targetView);
+ expect(event.targetElement).toBe(rootQuerySelector(selector));
+
+ expect(event.items).toBeUndefined();
+ });
+
+ it('onContextMenuPreparing fired on headerItem', () => {
+ const cardView = setup({ columns: ['Column 1'] });
+ const event = openContextMenu(cardView, SELECTORS.headerItem);
+
+ expect(event.card).toBeUndefined();
+ expect(event.cardIndex).toBeUndefined();
+ expect(event.column).toBeDefined();
+ expect(event.column?.name).toEqual('Column 1');
+ expect(event.columnIndex).toEqual(0);
+
+ expect(event.component).toBe(cardView);
+ expect(event.element).toBe(cardView.element());
+ expect(event.target).toEqual('headerPanel');
+ expect(event.targetElement).toBe(rootQuerySelector(SELECTORS.headerItem));
+
+ expect(event.items).toHaveLength(3);
+ expect(event.items).toMatchObject([
+ { value: 'asc', icon: 'sortuptext' },
+ { value: 'desc', icon: 'sortdowntext' },
+ { value: undefined, icon: 'none' },
+ ]);
+ });
+
+ it('onContextMenuPreparing fired on contentView card', () => {
+ const cardView = setup({
+ columns: [{ dataField: 'test' }],
+ keyExpr: 'id',
+ dataSource: [{ id: 10, test: 'some value 1' }, { id: 11, test: 'some value 2' }],
+ });
+ const event = openContextMenu(cardView, SELECTORS.card);
+
+ expect(event.card).toMatchObject({
+ key: 10,
+ index: 0,
+ data: { id: 10, test: 'some value 1' },
+ cells: [{ value: 'some value 1' }],
+ });
+ expect(event.cardIndex).toEqual(0);
+
+ expect(event.column).toBeUndefined();
+ expect(event.columnIndex).toBeUndefined();
+
+ expect(event.component).toBe(cardView);
+ expect(event.element).toBe(cardView.element());
+ expect(event.target).toEqual('content');
+ expect(event.targetElement).toBe(rootQuerySelector(SELECTORS.card));
+
+ expect(event.items).toBeUndefined();
+ });
+
+ it('columns have customized context menu items sorting text', () => {
+ const cardView = setup({
+ columns: [{ dataField: 'column1' }],
+ sorting: {
+ ascendingText: 'custom text 1',
+ descendingText: 'custom text 2',
+ clearText: 'custom text 3',
+ },
+ });
+ const event = openContextMenu(cardView, SELECTORS.headerItem);
+
+ expect(event.items).toHaveLength(3);
+ expect(event.items).toMatchObject([
+ { text: 'custom text 1' },
+ { text: 'custom text 2' },
+ { text: 'custom text 3' },
+ ]);
+ });
+
+ it.each<{ sortOrder?: SortOrder }>([
+ { sortOrder: 'asc' },
+ { sortOrder: 'desc' },
+ { sortOrder: undefined },
+ ])('context menu items disabled state when column sortOrder: $sortOrder', ({ sortOrder }) => {
+ const cardView = setup({ columns: [{ dataField: 'column1', sortOrder }] });
+ const event = openContextMenu(cardView, SELECTORS.headerItem);
+
+ expect(event.items).toHaveLength(3);
+
+ event.items?.forEach((item) => {
+ expect(item.disabled).toBe(sortOrder === item.value);
+ });
+ });
+
+ it('items set in onContextMenuPreparing are displayed', () => {
+ const cardView = setup({ });
+
+ let itemClickFired = false;
+
+ cardView.on('contextMenuPreparing', (e) => {
+ e.items = [{
+ text: 'custom item',
+ onItemClick: () => {
+ itemClickFired = true;
+ },
+ }];
+ });
+
+ openContextMenu(cardView, SELECTORS.contentView);
+
+ const contextMenu = getContextMenuInstance();
+ const contextMenuElement = getContextMenuElement();
+ const contextMenuItems = contextMenu.option('items');
+
+ expect(contextMenuItems).toHaveLength(1);
+ expect(contextMenuItems).toMatchObject([
+ { text: 'custom item' },
+ ]);
+
+ const firstItemElement = contextMenuElement.querySelector(SELECTORS.menuItem) as HTMLElement;
+ firstItemElement.click();
+
+ expect(itemClickFired).toBeTruthy();
+ });
+
+ it('onContextMenuPreparing event.column is correct when first column is invisible', () => {
+ const cardView = setup({
+ columns: [
+ { dataField: 'column1', visible: false },
+ { dataField: 'column2' },
+ ],
+ });
+ const event = openContextMenu(cardView, `${SELECTORS.headerItem}:nth-of-type(2)`);
+
+ expect(event.columnIndex).toEqual(1);
+ expect(event.column?.name).toEqual('column2');
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.ts b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.ts
new file mode 100644
index 000000000000..87e1f0b49f04
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/card_view/context_menu/view.ts
@@ -0,0 +1,12 @@
+import { BaseContextMenuView } from '../../grid_core/context_menu/view';
+import { ContextMenuController } from './controller';
+
+export class ContextMenuView extends BaseContextMenuView {
+ public static dependencies = [ContextMenuController] as const;
+
+ constructor(
+ protected readonly controller: ContextMenuController,
+ ) {
+ super(controller);
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/di.ts b/packages/devextreme/js/__internal/grids/new/card_view/di.ts
index 9556947ddc7e..6429e45b1919 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/di.ts
+++ b/packages/devextreme/js/__internal/grids/new/card_view/di.ts
@@ -1,8 +1,11 @@
/* eslint-disable spellcheck/spell-checker */
import type { DIContext } from '@ts/core/di';
+import { BaseContextMenuController } from '../grid_core/context_menu/controller';
import { register as gridCoreDIRegister } from '../grid_core/di';
import * as ContentViewModule from './content_view/index';
+import { ContextMenuController } from './context_menu/controller';
+import { ContextMenuView } from './context_menu/view';
import { HeaderPanelView } from './header_panel/view';
export function register(diContext: DIContext): void {
@@ -10,4 +13,8 @@ export function register(diContext: DIContext): void {
diContext.register(ContentViewModule.View);
diContext.register(HeaderPanelView);
+ diContext.register(ContextMenuView);
+
+ diContext.register(ContextMenuController);
+ diContext.register(BaseContextMenuController, ContextMenuController);
}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx
index b661f60e5489..6745b046506c 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx
@@ -36,6 +36,8 @@ export interface HeaderPanelProps {
visible: boolean;
draggingOptions?: DraggingOptions;
+
+ showContextMenu: (e: MouseEvent, column?: Column, columnIndex?: number) => void;
}
export class HeaderPanel extends Component {
@@ -45,7 +47,10 @@ export class HeaderPanel extends Component {
}
return (
-
+
{
scrollByContent={true}
>
- {this.props.columns.map((column) => (
+ {this.props.columns.map((column, index) => (
- {
element: Element,
callback?: () => void,
) => this.props.onFilterClick?.(element, column, callback)}
+ onContextMenu={(e) => {
+ this.props.showContextMenu(e, column, index);
+ }}
/>
))}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx
index 4579ed903000..d3fe17c14273 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx
@@ -74,6 +74,7 @@ export interface ItemProps {
element: Element,
onFilterCloseCallback?: () => void,
) => void;
+ onContextMenu?: (e: MouseEvent) => void;
}
export class Item extends Component {
@@ -105,6 +106,7 @@ export class Item extends Component {
this.onFilterKeyPressHandler,
)}
onKeyUp={this.keyboardHandler.onKeyUpHandler}
+ onContextMenu={this.props.onContextMenu}
>
{this.props.status && ICONS[this.props.status]}
{Template && }
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx
index b6aab4e32193..8a60198e4c54 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx
@@ -7,6 +7,7 @@ import { HeaderFilterController } from '@ts/grids/new/grid_core/filtering/header
import type { Column } from '../../grid_core/columns_controller/types';
import { SortingController } from '../../grid_core/sorting_controller/sorting_controller';
+import { ContextMenuController } from '../context_menu/controller';
import { OptionsController } from '../options_controller';
import type { HeaderPanelProps } from './header_panel';
import { HeaderPanel } from './header_panel';
@@ -19,6 +20,7 @@ export class HeaderPanelView extends View {
ColumnsController,
OptionsController,
HeaderFilterController,
+ ContextMenuController,
] as const;
constructor(
@@ -26,6 +28,7 @@ export class HeaderPanelView extends View {
private readonly columnsController: ColumnsController,
private readonly options: OptionsController,
private readonly headerFilterController: HeaderFilterController,
+ private readonly contextMenuController: ContextMenuController,
) {
super();
}
@@ -48,6 +51,7 @@ export class HeaderPanelView extends View {
visible: this.options.oneWay('headerPanel.visible'),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
draggingOptions: this.options.oneWay('headerPanel.dragging') as any,
+ showContextMenu: this.showContextMenu.bind(this),
});
}
@@ -83,4 +87,8 @@ export class HeaderPanelView extends View {
): void {
this.headerFilterController.openPopup(element, column, onFilterCloseCallback);
}
+
+ private showContextMenu(e: MouseEvent, column?: Column, columnIndex?: number): void {
+ this.contextMenuController.show(e, 'headerPanel', { column, columnIndex });
+ }
}
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx
index 8d8e122f05d4..6458d5778774 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx
@@ -12,6 +12,7 @@ import type { Config } from '../grid_core/core/config_context';
import { ConfigContext } from '../grid_core/core/config_context';
import { RootElementUpdater } from '../grid_core/inferno_wrappers/root_element_updater';
import { ContentView } from './content_view/view';
+import { ContextMenuView } from './context_menu/view';
import { HeaderPanelView } from './header_panel/view';
import { OptionsController } from './options_controller';
@@ -27,13 +28,14 @@ interface MainViewProps {
HeaderFilterPopup: ComponentType;
FilterPanel: ComponentType;
ColumnChooser: ComponentType;
+ ContextMenu: ComponentType;
config: Config;
rootElementRef: RefObject;
}
function MainViewComponent({
Toolbar, Content, Pager, HeaderPanel, HeaderFilterPopup,
- FilterPanel, ColumnChooser, config, rootElementRef,
+ FilterPanel, ColumnChooser, ContextMenu, config, rootElementRef,
}: MainViewProps): JSX.Element {
return (<>
@@ -57,6 +59,7 @@ function MainViewComponent({
+
>);
@@ -73,6 +76,7 @@ export class MainView extends View
{
HeaderFilterPopupView,
FilterPanelView,
ColumnChooserView,
+ ContextMenuView,
OptionsController,
] as const;
@@ -84,6 +88,7 @@ export class MainView extends View {
private readonly headerFilterPopup: HeaderFilterPopupView,
private readonly filterPanel: FilterPanelView,
private readonly columnsChooser: ColumnChooserView,
+ private readonly contextMenu: ContextMenuView,
private readonly options: OptionsController,
) {
super();
@@ -100,6 +105,7 @@ export class MainView extends View {
HeaderFilterPopup: this.headerFilterPopup.asInferno(),
FilterPanel: this.filterPanel.asInferno(),
ColumnChooser: this.columnsChooser.asInferno(),
+ ContextMenu: this.contextMenu.asInferno(),
config: combined({
rtlEnabled: this.options.oneWay('rtlEnabled'),
disabled: this.options.oneWay('disabled'),
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/options.ts b/packages/devextreme/js/__internal/grids/new/card_view/options.ts
index 592f6d4c5262..c9c3f0b1fd87 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/options.ts
+++ b/packages/devextreme/js/__internal/grids/new/card_view/options.ts
@@ -1,6 +1,7 @@
import * as GridCore from '@ts/grids/new/grid_core/options';
import * as ContentView from './content_view/index';
+import type * as ContextMenu from './context_menu/index';
import * as HeaderPanel from './header_panel/index';
/**
@@ -9,7 +10,8 @@ import * as HeaderPanel from './header_panel/index';
export type Options =
& GridCore.Options
& ContentView.Options
- & HeaderPanel.Options;
+ & HeaderPanel.Options
+ & ContextMenu.Options;
export const defaultOptions = {
...GridCore.defaultOptions,
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/widget.ts b/packages/devextreme/js/__internal/grids/new/card_view/widget.ts
index 912da32def9f..a71ce2af7eca 100644
--- a/packages/devextreme/js/__internal/grids/new/card_view/widget.ts
+++ b/packages/devextreme/js/__internal/grids/new/card_view/widget.ts
@@ -8,6 +8,8 @@ import { OptionsController as OptionsControllerBase } from '@ts/grids/new/grid_c
import { GridCoreNew } from '@ts/grids/new/grid_core/widget';
import * as ContentViewModule from './content_view/index';
+import { ContextMenuController } from './context_menu/controller';
+import { ContextMenuView } from './context_menu/view';
import * as di from './di';
import { HeaderPanelView } from './header_panel/view';
import { MainView } from './main_view';
@@ -19,6 +21,10 @@ export class CardViewBase extends GridCoreNew {
headerPanel!: HeaderPanelView;
+ contextMenu!: ContextMenuView;
+
+ contextMenuController!: ContextMenuController;
+
protected _registerDIContext(): void {
super._registerDIContext();
@@ -40,6 +46,8 @@ export class CardViewBase extends GridCoreNew {
super._initDIContext();
this.contentView = this.diContext.get(ContentViewModule.View);
this.headerPanel = this.diContext.get(HeaderPanelView);
+ this.contextMenu = this.diContext.get(ContextMenuView);
+ this.contextMenuController = this.diContext.get(ContextMenuController);
}
// eslint-disable-next-line @stylistic/max-len
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/test-utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/test-utils.ts
index f396f4071c89..65624a2f30fe 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/test-utils.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/test-utils.ts
@@ -2,6 +2,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { expect } from '@jest/globals';
+import { ContextMenuController } from '../../card_view/context_menu';
import { ColumnsController } from '../columns_controller';
import type { Options } from '../options';
import { OptionsControllerMock } from '../options_controller/options_controller.mock';
@@ -10,27 +11,35 @@ import { ToolbarView } from '../toolbar/view';
import { ColumnChooserController } from './controller';
import { ColumnChooserView } from './view';
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-const createToolbarView = (optionsController: OptionsControllerMock) => {
+const createToolbarView = (
+ columnsController: ColumnsController,
+ optionsController: OptionsControllerMock,
+): {
+ toolbarElement: HTMLDivElement;
+ toolbar: ToolbarView;
+ toolbarController: ToolbarController;
+} => {
const toolbarElement = document.createElement('div');
const toolbarController = new ToolbarController(optionsController);
- const toolbar = new ToolbarView(toolbarController, optionsController);
+ // @ts-expect-error
+
+ const contextMenuController = new ContextMenuController(columnsController, optionsController);
+ const toolbar = new ToolbarView(toolbarController, contextMenuController, optionsController);
return { toolbarElement, toolbar, toolbarController };
};
const createColumnChooserView = (
+ columnsController: ColumnsController,
optionsController: OptionsControllerMock,
toolbarController?: ToolbarController,
): {
columnChooserElement: HTMLDivElement;
columnChooser: ColumnChooserView;
columnChooserController: ColumnChooserController;
- columnsController: ColumnsController;
} => {
const columnChooserElement = document.createElement('div');
- const columnsController = new ColumnsController(optionsController);
const columnChooserController = new ColumnChooserController(columnsController, optionsController);
const columnChooser = new ColumnChooserView(
@@ -43,7 +52,6 @@ const createColumnChooserView = (
columnChooserElement,
columnChooser,
columnChooserController,
- columnsController,
};
};
@@ -55,9 +63,11 @@ export const renderColumnChooser = async (options?: Options): Promise<{
columnsController: ColumnsController;
}> => {
const optionsController = new OptionsControllerMock(options ?? {});
+ const columnsController = new ColumnsController(optionsController);
+
const {
- columnChooserElement, columnChooser, columnChooserController, columnsController,
- } = createColumnChooserView(optionsController);
+ columnChooserElement, columnChooser, columnChooserController,
+ } = createColumnChooserView(columnsController, optionsController);
columnChooser.render(columnChooserElement);
@@ -90,17 +100,18 @@ export const renderColumnChooserWithToolbar = (options?: Options): {
columnsController: ColumnsController;
} => {
const optionsController = new OptionsControllerMock(options ?? {});
+ const columnsController = new ColumnsController(optionsController);
+
const {
toolbarElement,
toolbar,
toolbarController,
- } = createToolbarView(optionsController);
+ } = createToolbarView(columnsController, optionsController);
const {
columnChooserElement,
columnChooser,
columnChooserController,
- columnsController,
- } = createColumnChooserView(optionsController, toolbarController);
+ } = createColumnChooserView(columnsController, optionsController, toolbarController);
const element = document.createElement('div');
element.append(toolbarElement, columnChooserElement);
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx
index 936bddfde638..f6463d32b3bd 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx
@@ -3,6 +3,7 @@
import type dxScrollable from '@js/ui/scroll_view/ui.scrollable';
import type { ScrollEventInfo } from '@js/ui/scroll_view/ui.scrollable';
import { combined, computed, state } from '@ts/core/reactive/index';
+import { ContextMenuController } from '@ts/grids/new/card_view/context_menu/index';
import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller';
import { View } from '@ts/grids/new/grid_core/core/view';
import { DataController } from '@ts/grids/new/grid_core/data_controller/index';
@@ -36,6 +37,7 @@ export abstract class ContentView extends View {
ColumnsController,
SelectionController,
ItemsController,
+ ContextMenuController,
] as const;
constructor(
@@ -45,6 +47,7 @@ export abstract class ContentView extends View {
protected readonly columnsController: ColumnsController,
protected readonly selectionController: SelectionController,
protected readonly itemsController: ItemsController,
+ protected readonly contextMenuController: ContextMenuController,
) {
super();
}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/context_menu.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/context_menu.tsx
new file mode 100644
index 000000000000..947835c91713
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/context_menu.tsx
@@ -0,0 +1,25 @@
+import type { Properties as ContextMenuProperties } from '@js/ui/context_menu';
+import type dxContextMenu from '@js/ui/context_menu';
+import type { RefObject } from 'inferno';
+
+import { CLASSES } from '../const';
+import { ContextMenu as ContextMenuComponent } from '../inferno_wrappers/context_menu';
+
+export type ContextMenuProps = ContextMenuProperties & {
+ componentRef: RefObject;
+};
+
+export function ContextMenu(props: ContextMenuProps): JSX.Element | null {
+ return (
+
+
+
+ );
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.mock.ts b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.mock.ts
new file mode 100644
index 000000000000..7d763aae2ddc
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.mock.ts
@@ -0,0 +1,15 @@
+import type { Item } from '@js/ui/context_menu';
+
+import { BaseContextMenuController } from './controller';
+
+export type TargetViewMock = 'view1' | 'view2';
+
+export interface ContextInfoMock { data: { test: string } }
+
+export class ContextMenuControllerMock extends BaseContextMenuController {
+ public static dependencies = [] as const;
+
+ public override getItems(): Item[] | undefined {
+ return undefined;
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.test.ts
new file mode 100644
index 000000000000..bd41e0953f46
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.test.ts
@@ -0,0 +1,88 @@
+import {
+ describe, expect, it, jest,
+} from '@jest/globals';
+import type { PositioningEvent } from '@js/ui/context_menu';
+import dxContextMenu from '@js/ui/context_menu';
+
+import { ContextMenuControllerMock } from './controller.mock';
+
+const setup = () => {
+ const controller = new ContextMenuControllerMock();
+
+ const container = document.createElement('div');
+ // eslint-disable-next-line new-cap
+ const contextMenu = new dxContextMenu(container, {
+ onPositioning: controller.onPositioning,
+ });
+
+ // @ts-expect-error
+ controller.contextMenuRef = { current: contextMenu };
+
+ return { controller, contextMenu };
+};
+
+describe('Core ContextMenu', () => {
+ describe('Controller', () => {
+ it('show()', () => {
+ const { controller, contextMenu } = setup();
+
+ jest.spyOn(controller, 'getItems').mockReturnValue([{ text: 'test1' }]);
+ jest.spyOn(controller, 'onPositioning');
+
+ const target = document.createElement('div');
+ const event = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ target,
+ };
+
+ const onShowing = jest.fn();
+ contextMenu.option('onShowing', onShowing);
+
+ controller.show(event as any, 'test view', { data: 'test' });
+
+ expect(controller.getItems).toHaveBeenCalledTimes(1);
+ expect(controller.getItems).toHaveBeenCalledWith('test view', target, { data: 'test' });
+
+ expect(event.preventDefault).toHaveBeenCalledTimes(1);
+ expect(event.stopPropagation).toHaveBeenCalledTimes(1);
+ expect(onShowing).toHaveBeenCalledTimes(1);
+
+ expect(contextMenu.option('items')).toEqual([{ text: 'test1' }]);
+ });
+
+ it('getItems() is called only once when show() is fired several times for the same event', () => {
+ const { controller } = setup();
+
+ jest.spyOn(controller, 'getItems').mockReturnValue([{ text: 'test1' }]);
+
+ const target = document.createElement('div');
+ const event = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ target,
+ };
+
+ controller.show(event as any, 'test view', { data: 'test' });
+ controller.show(event as any, 'test view', { data: 'test' });
+
+ expect(controller.getItems).toHaveBeenCalledTimes(1);
+ });
+
+ it('onPositioning() sets event to position.of', () => {
+ const { controller } = setup();
+
+ const event = {
+ target: document.createElement('div'),
+ };
+
+ controller.show(event as any, 'test view');
+
+ const e = { position: {} } as PositioningEvent;
+
+ controller.onPositioning(e);
+
+ expect(e.position.of).toBe(event);
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.ts
new file mode 100644
index 000000000000..01d153fecfb4
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/controller.ts
@@ -0,0 +1,43 @@
+import type { Item as ContextMenuItem, PositioningEvent } from '@js/ui/context_menu';
+import type dxContextMenu from '@js/ui/context_menu';
+import { createRef } from 'inferno';
+
+export abstract class BaseContextMenuController {
+ public readonly contextMenuRef = createRef();
+
+ private lastEvent?: MouseEvent;
+
+ public onPositioning = (e: PositioningEvent): void => {
+ // @ts-expect-error
+ e.position.of = this.lastEvent;
+ };
+
+ public show(event: MouseEvent, view: TTargetView, contextInfo?: TContextInfo): void {
+ const contextMenu = this.contextMenuRef.current;
+ const targetElement = event.target as Element;
+
+ if (event === this.lastEvent || !contextMenu || !targetElement) {
+ return;
+ }
+
+ this.lastEvent = event;
+
+ const items = this.getItems(view, targetElement, contextInfo);
+ contextMenu.option('items', items);
+
+ if (!items) {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ contextMenu.show().catch(console.error);
+ }
+
+ protected abstract getItems(
+ view: TTargetView,
+ targetElement: Element,
+ contextInfo?: TContextInfo
+ ): ContextMenuItem[] | undefined;
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/view.ts b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/view.ts
new file mode 100644
index 000000000000..e7afc2afee5e
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/view.ts
@@ -0,0 +1,45 @@
+import type {
+ InitializedEvent, Item as ItemClickEvent,
+} from '@js/ui/context_menu';
+import type { SubsGets } from '@ts/core/reactive/index';
+import { combined } from '@ts/core/reactive/index';
+
+import { View } from '../core/view';
+import type { ContextMenuProps } from './context_menu';
+import { ContextMenu } from './context_menu';
+import type { BaseContextMenuController } from './controller';
+
+const CLASS = {
+ contextMenu: 'dx-context-menu',
+};
+
+export abstract class BaseContextMenuView extends View {
+ protected override component = ContextMenu;
+
+ constructor(
+ protected readonly controller: BaseContextMenuController<{}, {}>,
+ ) {
+ super();
+ }
+
+ protected override getProps(): SubsGets {
+ return combined({
+ componentRef: this.controller.contextMenuRef,
+ cssClass: this.getWidgetContainerClass(),
+ onInitialized: (e: InitializedEvent) => {
+ // @ts-expect-error
+ e.component?.setAria('role', 'presentation');
+ e.component?.$element().addClass(CLASS.contextMenu);
+ },
+ onItemClick: (e: ItemClickEvent) => {
+ e.itemData?.onItemClick?.(e);
+ },
+ onPositioning: this.controller.onPositioning,
+ } as ContextMenuProps);
+ }
+
+ // TODO: move this to another place
+ private getWidgetContainerClass(): string {
+ return 'dx-cardview-container';
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/di.test_utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/di.test_utils.ts
index edc0040ae32f..7a8ccdd2a5cd 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/di.test_utils.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/di.test_utils.ts
@@ -1,6 +1,8 @@
/* eslint-disable spellcheck/spell-checker */
import { DIContext } from '@ts/core/di';
+import { BaseContextMenuController } from './context_menu/controller';
+import { ContextMenuControllerMock } from './context_menu/controller.mock';
import { register } from './di';
import type { Options } from './options';
import { OptionsController } from './options_controller/options_controller';
@@ -14,5 +16,7 @@ export function getContext(config: Options): DIContext {
diContext.registerInstance(OptionsController, options);
diContext.registerInstance(OptionsControllerMock, options);
+ diContext.register(BaseContextMenuController, ContextMenuControllerMock);
+
return diContext;
}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/inferno_wrappers/context_menu.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/inferno_wrappers/context_menu.tsx
new file mode 100644
index 000000000000..1512ee4053bf
--- /dev/null
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/inferno_wrappers/context_menu.tsx
@@ -0,0 +1,12 @@
+import type { Properties as ContextMenuProperties } from '@js/ui/context_menu';
+import dxContextMenu from '@js/ui/context_menu';
+
+import { InfernoWrapper } from './widget_wrapper';
+
+export class ContextMenu extends InfernoWrapper {
+ private readonly contentRef: { current?: HTMLDivElement } = {};
+
+ protected getComponentFabric(): typeof dxContextMenu {
+ return dxContextMenu;
+ }
+}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/toolbar.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/toolbar.tsx
index c159b6e68494..c33d49becab9 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/toolbar.tsx
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/toolbar.tsx
@@ -1,8 +1,39 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { off, on } from '@js/common/core/events';
+import { Component, createRef } from 'inferno';
+
import { Toolbar } from '../inferno_wrappers/toolbar';
import type { ToolbarProps } from './types';
-export function ToolbarView(props: ToolbarProps): JSX.Element {
- return (
- props.visible ? : <>>
- );
+export class ToolbarView extends Component {
+ private readonly containerRef = createRef();
+
+ public render(): JSX.Element {
+ const { visible, items, disabled } = this.props;
+
+ if (!visible) {
+ return <>>;
+ }
+
+ return (
+
+ );
+ }
+
+ public componentDidMount(): void {
+ on(this.containerRef.current!, 'dxcontextmenu', this.handleContextMenu);
+ }
+
+ public componentWillUnmount(): void {
+ off(this.containerRef.current!, 'dxcontextmenu', this.handleContextMenu);
+ }
+
+ private readonly handleContextMenu = (e: MouseEvent): void => {
+ this.props.showContextMenu!(e);
+ };
}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts
index 994bf1791e7f..9cd998c2c68e 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/types.ts
@@ -15,4 +15,5 @@ export interface ToolbarProps {
items?: ToolbarItems;
visible?: boolean | undefined;
disabled?: boolean;
+ showContextMenu?: (e: MouseEvent) => void;
}
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx
index 588c57243783..f0ec95d3a14a 100644
--- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx
+++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx
@@ -1,7 +1,7 @@
-/* eslint-disable spellcheck/spell-checker */
import type { SubsGets } from '@ts/core/reactive/index';
import { combined, computed } from '@ts/core/reactive/index';
+import { BaseContextMenuController } from '../context_menu/controller';
import { View } from '../core/view';
import { OptionsController } from '../options_controller/options_controller';
import { ToolbarController } from './controller';
@@ -19,10 +19,15 @@ export class ToolbarView extends View {
[this.visibleConfig, this.controller.items],
);
- public static dependencies = [ToolbarController, OptionsController] as const;
+ public static dependencies = [
+ ToolbarController,
+ BaseContextMenuController,
+ OptionsController,
+ ] as const;
constructor(
private readonly controller: ToolbarController,
+ private readonly contextMenuController: BaseContextMenuController,
private readonly options: OptionsController,
) {
super();
@@ -33,6 +38,11 @@ export class ToolbarView extends View {
visible: this.visible,
items: this.controller.items,
disabled: this.options.oneWay('toolbar.disabled'),
+ showContextMenu: this.showContextMenu.bind(this),
});
}
+
+ private showContextMenu(e: MouseEvent): void {
+ this.contextMenuController.show(e, 'toolbar');
+ }
}