Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CardView: Implement Selection #29325

Draft
wants to merge 2 commits into
base: grids/cardview/draft
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@
background-color: $cardview-card-background-color;
overflow: hidden;
}

.dx-cardview-card-selection {
background-color: $cardview-card-selection-background-color;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ $cardview-card-border-size: null !default;
$cardview-card-min-width: null !default;
$cardview-card-border-radius: null !default;
$cardview-card-background-color: null !default;
$cardview-card-selection-background-color: null !default;
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,27 @@
}
}
}

.dx-cardview-select-checkboxes-hidden .dx-cardview-card:not(.dx-cardview-card-selection) .dx-cardview-select-checkbox {
.dx-checkbox {
display: none;
}

.dx-toolbar-item-content::before {
content: '';
width: 20px;
height: 20px;
display: inline-block;
pointer-events: none;
}

.dx-toolbar-item-content:hover {
&::before {
display: none;
}

.dx-checkbox {
display: inline-block;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
@use '../../base/cardView/content_view/content/variables' as *;

// adduse

$cardview-background-color: $base-typography-bg !default;

$cardview-header-item-background-color: #F0F0F0 !default;
Expand All @@ -24,3 +23,5 @@ $cardview-header-filter-icon-selected-color: $base-accent !default;

$cardview-card-content-field-value-highlight-color: $base-inverted-text-color !default;
$cardview-card-content-field-value-highlight-background: $base-accent !default;

$cardview-card-selection-background-color: #EBF3FC;
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
/* eslint-disable @typescript-eslint/init-declarations */

import { describe, expect, it } from '@jest/globals';
import { render } from 'inferno';
import { createRef, render } from 'inferno';

import { Card } from './card';
import { Card, CLASSES } from './card';

const mockOnDblClick = {
const createMockCallback = () => ({
called: false,
call() {
call(): void {
this.called = true;
},
};
});

const mockOnClick = {
called: false,
call() {
this.called = true;
},
};
const mockSelectCard = createMockCallback();
const mockOnDblClick = createMockCallback();
const mockOnClick = createMockCallback();
const mockOnHold = createMockCallback();

const props = {
row: {
Expand Down Expand Up @@ -70,12 +68,10 @@ const props = {
maxWidth: 300,
width: 300,
minWidth: 300,
onDblClick: mockOnDblClick.call(),
onClick: mockOnClick.call(),
};

const CLASSES = {
card: 'dx-cardview-card',
selectCard: mockSelectCard.call.bind(mockSelectCard),
onDblClick: mockOnDblClick.call.bind(mockOnDblClick),
onClick: mockOnClick.call.bind(mockOnClick),
onHold: mockOnHold.call.bind(mockOnHold),
};

describe('Events', () => {
Expand All @@ -84,29 +80,32 @@ describe('Events', () => {
beforeEach(() => {
container = document.createElement('div');
// @ts-expect-error
render(<Card {...props} />, container);
render(<Card {...{ ...props, elementRef: createRef() } } />, container);
});

it('should trigger onClick event', () => {
// @ts-expect-error
render(<Card {...props} />, container);

const cardElement = container.querySelector(`.${CLASSES.card}`);
cardElement?.dispatchEvent(new MouseEvent('click'));

expect(mockOnClick.called).toBe(true);
});

it('should trigger onDblClick event', () => {
// @ts-expect-error
render(<Card {...props} />, container);

it.skip('should trigger onDblClick event', () => {
const cardElement = container.querySelector(`.${CLASSES.card}`);

cardElement?.dispatchEvent(new MouseEvent('dblclick'));

expect(mockOnDblClick.called).toBe(true);
});

it('should trigger onHold event', () => {
const cardElement = container.querySelector(`.${CLASSES.card}`);

cardElement?.dispatchEvent(new MouseEvent('dxhold'));

expect(mockOnHold.called).toBe(true);
});

it('should trigger onHoverChanged event on mouse enter', () => {
const mockHover: { called: boolean; fn: ({ isHovered }: { isHovered: boolean }) => void } = {
called: false,
Expand Down Expand Up @@ -161,3 +160,42 @@ describe('Events', () => {
expect(fieldValue?.textContent).toBe('devextreme');
});
});

describe('Callbacks', () => {
describe('selectCard', () => {
// @ts-expect-errors
beforeEach(() => {
mockSelectCard.called = false;
});

describe('when allowSelectOnClick = true', () => {
it('should rise it', () => {
const container = document.createElement('div');
const newProps = { ...props, elementRef: createRef(), allowSelectOnClick: true };
// @ts-expect-error
render(<Card {...newProps} />, container);

const cardElement = container.querySelector(`.${CLASSES.card}`);

cardElement?.dispatchEvent(new MouseEvent('click'));

expect(mockSelectCard.called).toBe(true);
});
});

describe('when allowSelectOnClick = false', () => {
it('should not rise it', () => {
const container = document.createElement('div');
const newProps = { ...props, elementRef: createRef(), allowSelectOnClick: false };
// @ts-expect-error
render(<Card {...newProps} />, container);

const cardElement = container.querySelector(`.${CLASSES.card}`);

cardElement?.dispatchEvent(new MouseEvent('click'));

expect(mockSelectCard.called).toBe(false);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */

import { isCommandKeyPressed } from '@js/common/core/events/utils/index';
import { off, on } from '@js/events';
import { combineClasses } from '@ts/core/utils/combine_classes';
import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types';
import type { DataObject } from '@ts/grids/new/grid_core/data_controller/types';
import { CollectionController } from '@ts/grids/new/grid_core/keyboard_navigation/collection_controller';
import type { InfernoNode, RefObject } from 'inferno';
import { Component, createRef } from 'inferno';

import type { SelectCardOptions } from '../../types';
import { Cover } from './cover';
import { Field } from './field';
import type { CardHeaderItem } from './header';
Expand All @@ -16,10 +18,11 @@ export const CLASSES = {
card: 'dx-cardview-card',
cardHover: 'dx-cardview-card-hoverable',
content: 'dx-cardview-card-content',
selectCard: 'dx-cardview-card-selection',
};

export interface CardClickEvent {
event: MouseEvent;
event?: MouseEvent;
row: DataRow;
}

Expand All @@ -35,6 +38,8 @@ export interface CardPreparedEvent {
export interface CardProps {
row: DataRow;

allowSelectOnClick?: boolean;

cover?: {
imageExpr?: (data: DataObject) => string;

Expand All @@ -54,15 +59,23 @@ export interface CardProps {

toolbar?: CardHeaderItem[];

width?: number;

isCheckBoxesRendered?: boolean;

template?: (row: DataRow) => JSX.Element;

onClick?: (e: CardClickEvent) => void;

onHold?: (e: CardClickEvent) => void;

onDblClick?: (e: CardClickEvent) => void;

onHoverChanged?: (e: CardHoverEvent) => void;

onPrepared?: (e: CardPreparedEvent) => void;

selectCard?: (row: DataRow, options: SelectCardOptions) => void;
}

export class Card extends Component<CardProps> {
Expand All @@ -83,12 +96,14 @@ export class Card extends Component<CardProps> {
fieldTemplate: FieldTemplate = Field,
hoverStateEnabled,
cover,
row,
} = this.props;

const className = [
CLASSES.card,
hoverStateEnabled ? CLASSES.cardHover : '',
].filter(Boolean).join(' ');
const className = combineClasses({
[CLASSES.card]: true,
[CLASSES.cardHover]: !!hoverStateEnabled,
[CLASSES.selectCard]: !!row.isSelected,
});

const imageSrc = cover?.imageExpr?.(this.props.row.data);
const alt = cover?.altExpr?.(this.props.row.data);
Expand All @@ -99,13 +114,15 @@ export class Card extends Component<CardProps> {
tabIndex={0}
ref={this.props.elementRef}
onKeyDown={(e): void => this.keyboardController.onKeyDown(e)}
onClick={this.handleClick}
onDblClick={this.handleDoubleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<CardHeader
row={row}
items={this.props.toolbar || []}
isCheckBoxesRendered={this.props.isCheckBoxesRendered}
selectCard={this.props.selectCard}
/>
{imageSrc && (
<Cover
Expand Down Expand Up @@ -136,9 +153,24 @@ export class Card extends Component<CardProps> {
componentDidMount(): void {
this.updateKeyboardController();
const { onPrepared } = this.props;

if (onPrepared) {
onPrepared({ instance: this });
}

on(this.containerRef.current!, 'dxclick', this.handleClick);

if (this.props.onHold) {
on(this.containerRef.current!, 'dxhold', this.handleHold);
}
}

componentWillUnmount(): void {
off(this.containerRef.current!, 'dxclick', this.handleClick);

if (this.props.onHold) {
off(this.containerRef.current!, 'dxhold', this.handleHold);
}
}

componentDidUpdate(): void {
Expand All @@ -158,12 +190,29 @@ export class Card extends Component<CardProps> {
};

handleClick = (event: MouseEvent): void => {
const { onClick, row } = this.props;
const {
allowSelectOnClick,
onClick,
selectCard,
row,
} = this.props;

onClick?.({ event, row });

if (allowSelectOnClick) {
selectCard?.(row, { control: isCommandKeyPressed(event), shift: event.shiftKey });
}
};

handleDoubleClick = (event: MouseEvent): void => {
const { onDblClick, row } = this.props;
onDblClick?.({ event, row });
};

handleHold = (event: MouseEvent): void => {
const { onHold, row } = this.props;

onHold?.({ event, row });
event.stopPropagation();
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-extraneous-dependencies */

import { describe, expect, it } from '@jest/globals';
import { render } from 'inferno';

Expand Down Expand Up @@ -68,4 +68,20 @@ describe('CardHeader', () => {
expect(customHeader).not.toBeNull();
expect(customHeader?.textContent).toBe('Custom Header');
});

it('should render a selection checkbox', () => {
const container = document.createElement('div');
render(
<CardHeader
visible
isCheckBoxesRendered
// @ts-expect-error
row={{ name: 'Card Title' }}
/>,
container,
);

const checkboxItem = container.querySelector('.dx-cardview-select-checkbox');
expect(checkboxItem).not.toBeNull();
});
});
Loading
Loading