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 a3748ebef495..1fc038f60400 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 @@ -5,6 +5,39 @@ exports[`common initial render should be successfull 1`] = ` class="dx-widget dx-cardview" > +
+
+
+
+
+ +
+
+
diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap new file mode 100644 index 000000000000..ac76d2654bdb --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Item should render sort icons 1`] = ` +
+
+ Column 1 + + + +
+
+`; + +exports[`Item should use column caption as text 1`] = ` +
+
+ my column caption +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap new file mode 100644 index 000000000000..3e8eeaf3d148 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColumnProperties headerItemCssClass should override content of headerPanel item 1`] = ` +
+
+
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+ +
+
+`; + +exports[`Options headerPanel itemCssClass should add css class to headerPanel item 1`] = ` +
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+`; + +exports[`Options headerPanel visible when it is false should hide headerPanel 1`] = ` +
+ +
+`; + +exports[`Options headerPanel visible when it is true should show headerPanel 1`] = ` +
+
+
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+ +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx new file mode 100644 index 000000000000..01f35f1f620a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx @@ -0,0 +1,144 @@ +import $ from '@js/core/renderer'; +import type * as SortableTypes from '@js/ui/sortable_types'; +import type { ComponentType, InfernoNode } from 'inferno'; +import { Component, render } from 'inferno'; + +import type { Column } from '../../grid_core/columns_controller/types'; +import type { Props as SortableProps } from '../../grid_core/inferno_wrappers/sortable'; +import { Sortable } from '../../grid_core/inferno_wrappers/sortable'; + +export type Status = 'forbid' | 'show' | 'moving' | 'none'; + +export interface Props extends Omit { + source: string; + + visibleColumns: Column[]; + + allowColumnReordering: boolean; + + onMove: (column: Column, toIndex: number, source: string) => void; + + dragTemplate?: ComponentType<{ column: Column; status: Status }>; +} + +interface State { + status: Status; +} + +export class ColumnSortable extends Component { + status: Status = 'moving'; + + dragItemProps?: { + container: HTMLElement; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: any; + }; + + private readonly onDragStart = (e: SortableTypes.DragStartEvent): void => { + const column = this.props.visibleColumns[e.fromIndex]; + + if (!column.allowReordering) { + e.cancel = true; + return; + } + + e.itemData = { + column, + source: this.props.source, + }; + }; + + private readonly onDragMove = (e: SortableTypes.DragMoveEvent): void => { + const containerCoords = $(e.element).get(0).getBoundingClientRect(); + const dragCoords = { + // @ts-expect-error + x: e.event.clientX, + // @ts-expect-error + y: e.event.clientY, + }; + + const yDistance = Math.min( + Math.abs(dragCoords.y - containerCoords.y), + Math.abs(dragCoords.y - containerCoords.y + containerCoords.height), + ); + + // TODO: move to scss variable + this.status = yDistance <= 64 + ? 'moving' + : 'forbid'; + + this.renderDragTemplate(); + }; + + private readonly onDragChange = (e: SortableTypes.DragChangeEvent): void => { + if (this.status === 'forbid') { + e.cancel = true; + } + }; + + private readonly onMove = (e: SortableTypes.AddEvent | SortableTypes.ReorderEvent): void => { + this.props.onMove( + e.itemData.column, + e.toIndex, + e.itemData.source, + ); + }; + + // TODO: move all none-native approaches to sortable wrapper + private readonly renderDragTemplate = (): void => { + if (!this.dragItemProps) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const DragTemplate = this.props.dragTemplate!; + render( + // @ts-expect-error + , + this.dragItemProps.container, + ); + }; + + render(): InfernoNode { + if (!this.props.allowColumnReordering) { + // @ts-expect-error + return this.props.children; + } + + const { + source, + visibleColumns, + dragTemplate, + dropFeedbackMode, + ...restProps + } = this.props; + + const sortableDragTemplate = dragTemplate ? (e, container): void => { + this.dragItemProps = { + props: e, + // @ts-expect-error + container: $(container).get(0), + }; + this.renderDragTemplate(); + } : undefined; + + return ( + + {this.props.children} + + ); + } +} 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 new file mode 100644 index 000000000000..4f87b0c4f2ea --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx @@ -0,0 +1,77 @@ +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; +import { Scrollable } from '@ts/grids/new/grid_core/inferno_wrappers/scrollable'; +import type { ComponentType } from 'inferno'; +import { Component } from 'inferno'; + +import { ColumnSortable } from './column_sortable'; +import { CLASSES as itemClasses, Item } from './item'; +import type { DraggingOptions } from './options'; + +export const CLASSES = { + headers: 'dx-cardview-headers', + content: 'dx-cardview-headers-content', +}; + +export interface HeaderPanelProps { + columns: Column[]; + + onMove: (column: Column, toIndex: number) => void; + + allowColumnReordering: boolean; + + showSortIndexes: boolean; + + onSortClick: (column: Column) => void; + + itemTemplate?: ComponentType<{ column: Column }>; + + itemCssClass?: string; + + visible: boolean; + + draggingOptions?: DraggingOptions; +} + +export class HeaderPanel extends Component { + public render(): JSX.Element { + if (!this.props.visible) { + return <>; + } + + return ( +
+ this.props.onMove?.(column, index)} + filter={`.${itemClasses.item}`} + dragTemplate={Item} + > + +
+ {this.props.columns.map((column) => ( + { this.props.onSortClick(column); }} + template={this.props.itemTemplate} + cssClass={this.props.itemCssClass} + /> + ))} +
+
+
+
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts new file mode 100644 index 000000000000..66aec0658fc2 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts @@ -0,0 +1,2 @@ +export * from './options'; +export { HeaderPanelView as View } from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx new file mode 100644 index 000000000000..c629938c7f5c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { normalizeColumn } from '../../grid_core/columns_controller/columns_controller.mock'; +import type { ItemProps } from './item'; +import { Item } from './item'; + +const setup = (props: ItemProps) => { + const rootElement = document.createElement('div'); + render( + , + rootElement, + ); + + return rootElement; +}; + +describe('Item', () => { + it('should use column caption as text', () => { + const el = setup({ + column: normalizeColumn({ + dataField: 'my column data field', + caption: 'my column caption', + }), + }); + + expect(el).toMatchSnapshot(); + }); + + it('should render sort icons', () => { + const el = setup({ + column: normalizeColumn({ + dataField: 'column1', + sortIndex: 0, + sortOrder: 'asc', + }), + }); + + expect(el).toMatchSnapshot(); + }); +}); 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 new file mode 100644 index 000000000000..6b927ba75c59 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx @@ -0,0 +1,86 @@ +import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { ComponentType } from 'inferno'; + +import type { Status } from './column_sortable'; + +export const CLASSES = { + item: 'dx-cardview-header-item', + button: 'dx-cardview-header-item-button', + sorting: { + container: 'dx-cardview-header-item-sorting', + order: 'dx-cardview-header-item-sorting-order', + }, +}; + +// TODO: extract icons to separate component +const ICONS = { + // TODO: move to dx-icon once they are updated + forbid: ( + + + + ), + // TODO: move to dx-icon once they are updated + moving: ( + + + + ), + sortUp: , + sortDown: , +}; + +interface SortIconProps { + sortOrder: 'asc' | 'desc'; + sortIndex: number; + showSortIndex: boolean; +} + +function SortIcon(props: SortIconProps): JSX.Element { + return ( + + {props.sortOrder === 'asc' && ICONS.sortUp} + {props.sortOrder === 'desc' && ICONS.sortDown} + { + props.showSortIndex && ( + + {props.sortIndex} + + ) + } + + ); +} + +export interface ItemProps { + column: Column; + status?: Status; + showSortIndexes?: boolean; + onSortClick?: () => void; + template?: ComponentType<{ column: Column }>; + cssClass?: string; +} + +export function Item(props: ItemProps): JSX.Element { + const Template = props.column.headerItemTemplate ?? props.template; + const cssClass = `${CLASSES.item} ${props.column.headerItemCssClass ?? ''} ${props.cssClass ?? ''}`; + + return ( +
+ { props.status && ICONS[props.status]} + {/* @ts-expect-error */} + { Template &&