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 &&