Skip to content

Commit e526921

Browse files
Stepper: render icons according to the isValid property value (#29338)
1 parent 2e9be59 commit e526921

File tree

5 files changed

+120
-52
lines changed

5 files changed

+120
-52
lines changed

packages/devextreme-scss/scss/widgets/base/stepper/layout/connector/_index.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
.dx-stepper-connector {
22
position: absolute;
3-
z-index: -1;
43

54
&.dx-stepper-connector-horizontal {
65
height: 2px;

packages/devextreme-scss/scss/widgets/base/stepper/layout/step/_index.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
&::before {
3535
content: '';
3636
position: absolute;
37-
z-index: -1;
3837
border-width: 1px;
3938
border-style: solid;
4039
border-radius: 999em;

packages/devextreme/js/__internal/ui/stepper/stepper.ts

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { Orientation } from '@js/common';
1+
import type { Orientation, SingleOrNone } from '@js/common';
22
import registerComponent from '@js/core/component_registrator';
33
import type { DxElement } from '@js/core/element';
44
import $, { type dxElementWrapper } from '@js/core/renderer';
55
import { Deferred } from '@js/core/utils/deferred';
66
import { isDefined } from '@js/core/utils/type';
77
import type { DxEvent } from '@js/events';
8-
import type { Item } from '@js/ui/stepper';
8+
import type { Item, Properties } from '@js/ui/stepper';
99
import { BindableTemplate } from '@ts/core/templates/m_bindable_template';
1010
import type { Template } from '@ts/core/templates/m_template';
1111
import { getImageContainer } from '@ts/core/utils/m_icon';
@@ -17,10 +17,11 @@ import type {
1717
} from '@ts/ui/collection/collection_widget.base';
1818
import CollectionWidgetAsync from '@ts/ui/collection/m_collection_widget.async';
1919
import Connector from '@ts/ui/stepper/connector';
20-
import type { StepperItemProperties } from '@ts/ui/stepper/stepper_item';
21-
import StepperItem, { STEP_COMPLETED_CLASS } from '@ts/ui/stepper/stepper_item';
22-
23-
import type { CollectionWidgetEditProperties } from '../collection/m_collection_widget.edit';
20+
import StepperItem, {
21+
STEP_COMPLETED_CLASS,
22+
STEP_INVALID_ICON,
23+
STEP_VALID_ICON,
24+
} from '@ts/ui/stepper/stepper_item';
2425

2526
export const STEPPER_CLASS = 'dx-stepper';
2627
export const STEP_LIST_CLASS = 'dx-step-list';
@@ -41,10 +42,12 @@ export const ORIENTATION: Record<string, Orientation> = {
4142
vertical: 'vertical',
4243
};
4344

44-
export interface StepperProperties extends CollectionWidgetEditProperties<Stepper> {
45-
orientation?: Orientation;
46-
linear?: boolean;
47-
isValidExpr?: (data: Item) => boolean | undefined;
45+
export interface StepperProperties extends Properties {
46+
selectionMode?: SingleOrNone;
47+
48+
loopItemFocus?: boolean;
49+
50+
selectionRequired?: boolean;
4851
}
4952

5053
class Stepper extends CollectionWidgetAsync<StepperProperties> {
@@ -66,9 +69,6 @@ class Stepper extends CollectionWidgetAsync<StepperProperties> {
6669
focusStateEnabled: true,
6770
loopItemFocus: false,
6871
selectionRequired: true,
69-
isValidExpr(data): boolean | undefined {
70-
return data ? data.isValid : undefined;
71-
},
7272
};
7373
}
7474

@@ -83,17 +83,36 @@ class Stepper extends CollectionWidgetAsync<StepperProperties> {
8383
};
8484
}
8585

86-
_prepareDefaultItemTemplate(data: StepperItemProperties, $container: dxElementWrapper): void {
86+
_getStepIcon(data: Item): string | undefined {
87+
const { isValid, icon } = data;
88+
89+
if (isValid === false) {
90+
return STEP_INVALID_ICON;
91+
}
92+
93+
if (isValid === true) {
94+
return STEP_VALID_ICON;
95+
}
96+
97+
return icon;
98+
}
99+
100+
_prepareDefaultItemTemplate(data: Item, $container: dxElementWrapper): void {
101+
const { text, title } = data;
102+
87103
const $indicatorElement = $('<div>').addClass(STEP_INDICATOR_CLASS);
88-
const $iconElement = getImageContainer(data.icon) ?? $('<div>').addClass(STEP_TEXT_CLASS).text(data.text ?? '');
104+
105+
const iconName = this._getStepIcon(data);
106+
const $iconElement = getImageContainer(iconName) ?? $('<div>').addClass(STEP_TEXT_CLASS).text(text ?? '');
89107

90108
$iconElement.appendTo($indicatorElement);
109+
91110
$indicatorElement.prependTo($container);
92111

93-
if (isDefined(data.title)) {
112+
if (isDefined(title)) {
94113
const $stepTitleDiv = $('<div>').addClass(STEP_TITLE_CLASS);
95114

96-
$stepTitleDiv.text(data.title);
115+
$stepTitleDiv.text(title);
97116

98117
$stepTitleDiv.appendTo($container);
99118
}
@@ -103,15 +122,18 @@ class Stepper extends CollectionWidgetAsync<StepperProperties> {
103122
super._initTemplates();
104123

105124
this._templateManager.addDefaultTemplates({
106-
item: new BindableTemplate(($container: dxElementWrapper, data: StepperItemProperties) => {
125+
item: new BindableTemplate((
126+
$container: dxElementWrapper,
127+
data: Item,
128+
) => {
107129
this._prepareDefaultItemTemplate(data, $container);
108130
}, ['text', 'icon', 'title', 'isValid'], this.option('integrationOptions.watchMethod')),
109131
});
110132
}
111133

112134
_createItemByTemplate(
113135
itemTemplate: Template,
114-
renderArgs: ItemRenderInfo<StepperItemProperties>,
136+
renderArgs: ItemRenderInfo<Item>,
115137
): DxElement {
116138
const { itemData, index } = renderArgs;
117139

@@ -128,13 +150,26 @@ class Stepper extends CollectionWidgetAsync<StepperProperties> {
128150
return StepperItem.getInstance<StepperItem>($item);
129151
}
130152

153+
_renderItem(
154+
index: number,
155+
itemData: Item,
156+
$container: dxElementWrapper,
157+
$itemToReplace: dxElementWrapper,
158+
): dxElementWrapper {
159+
const $itemFrame = super._renderItem(index, itemData, $container, $itemToReplace);
160+
161+
this._getItemInstance($itemFrame).updateInvalidClass(itemData.isValid);
162+
163+
return $itemFrame;
164+
}
165+
131166
_postprocessRenderItem(args: PostprocessRenderItemInfo<StepperItem>): void {
132167
super._postprocessRenderItem(args);
133168

134169
const { selectedIndex = 0 } = this.option();
135-
const $itemInstance = this._getItemInstance(args.itemElement);
170+
const itemInstance = this._getItemInstance(args.itemElement);
136171

137-
$itemInstance.changeCompleted(args.itemIndex < selectedIndex);
172+
itemInstance.changeCompleted(args.itemIndex < selectedIndex);
138173
}
139174

140175
_itemClass(): string {
@@ -276,9 +311,9 @@ class Stepper extends CollectionWidgetAsync<StepperProperties> {
276311
const isCompleted = lastCompletedIndex < selectedIndex;
277312

278313
for (let i = startIndex; i < endIndex; i += 1) {
279-
const $itemInstance = this._getItemInstance($(itemElements[i]));
314+
const itemInstance = this._getItemInstance($(itemElements[i]));
280315

281-
$itemInstance.changeCompleted(isCompleted);
316+
itemInstance.changeCompleted(isCompleted);
282317
}
283318
}
284319

@@ -296,12 +331,25 @@ class Stepper extends CollectionWidgetAsync<StepperProperties> {
296331
}
297332

298333
_itemOptionChanged(
299-
item: StepperItemProperties,
300-
property: keyof StepperItemProperties,
334+
item: Item,
335+
property: keyof Item,
301336
value: unknown,
302337
prevValue: unknown,
303338
): void {
304339
switch (property) {
340+
case 'isValid': {
341+
type PropertyType = Item[typeof property];
342+
343+
const itemIndex = this._getIndexByItem(item);
344+
const $item = $(this._itemElements()[itemIndex]);
345+
346+
const itemInstance = this._getItemInstance($item);
347+
348+
itemInstance.updateInvalidClass(value as PropertyType);
349+
350+
super._itemOptionChanged(item, property, value, prevValue);
351+
break;
352+
}
305353
default:
306354
super._itemOptionChanged(item, property, value, prevValue);
307355
}
@@ -318,9 +366,6 @@ class Stepper extends CollectionWidgetAsync<StepperProperties> {
318366
break;
319367
case 'linear':
320368
break;
321-
case 'isValidExpr':
322-
this._invalidate();
323-
break;
324369
default:
325370
super._optionChanged(args);
326371
}

packages/devextreme/js/__internal/ui/stepper/stepper_item.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,14 @@
1-
import type {
2-
CollectionWidgetItem as CollectionWidgetItemProperties,
3-
} from '@js/ui/collection/ui.collection_widget.base';
1+
import type { Item } from '@js/ui/stepper';
42
import CollectionWidgetItem from '@ts/ui/collection/m_item';
53

64
export const STEP_COMPLETED_CLASS = 'dx-step-completed';
75
export const STEP_INVALID_CLASS = 'dx-step-invalid';
6+
export const STEP_VALID_ICON = 'check';
7+
export const STEP_INVALID_ICON = 'errorcircle';
88

9-
export interface StepperItemProperties extends CollectionWidgetItemProperties {
10-
icon?: string;
11-
title?: string;
12-
isValid?: boolean;
13-
}
14-
15-
class StepperItem extends CollectionWidgetItem<StepperItemProperties> {
16-
_renderWatchers(): void {
17-
super._renderWatchers();
18-
19-
this._startWatcher('isValid', this._renderValidState.bind(this));
20-
}
21-
22-
_renderValidState(
23-
value: boolean | undefined,
24-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25-
oldValue?: boolean | undefined,
26-
): void {
27-
this._$element.toggleClass(STEP_INVALID_CLASS, value !== undefined && !value);
9+
class StepperItem extends CollectionWidgetItem<Item> {
10+
updateInvalidClass(isValid: boolean | undefined): void {
11+
this._$element.toggleClass(STEP_INVALID_CLASS, isValid !== undefined && !isValid);
2812
}
2913

3014
changeCompleted(isCompleted: boolean): void {

packages/devextreme/testing/tests/DevExpress.ui.widgets/stepper.markup.tests.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
import {
1616
STEP_COMPLETED_CLASS,
1717
STEP_INVALID_CLASS,
18+
STEP_VALID_ICON,
19+
STEP_INVALID_ICON,
1820
} from '__internal/ui/stepper/stepper_item';
1921

2022
const STEP_CONTENT_CLASS = 'dx-step-content';
@@ -292,4 +294,43 @@ QUnit.module('Step.isValid', moduleConfig, () => {
292294

293295
assert.strictEqual(this.getItems().eq(0).hasClass(STEP_INVALID_CLASS), false, `${STEP_INVALID_CLASS} is removed`);
294296
});
297+
298+
[
299+
{ options: { isValid: true, icon: 'test', text: 'test' }, expectedIcon: STEP_VALID_ICON },
300+
{ options: { isValid: false, icon: 'test', text: 'test' }, expectedIcon: STEP_INVALID_ICON },
301+
{ options: { isValid: undefined, icon: 'test', text: 'test' }, expectedIcon: 'test' },
302+
].forEach(({ options, expectedIcon }) => {
303+
QUnit.test(`Step indicator should contain '${expectedIcon}' icon, step options: ${JSON.stringify(options)}`, function(assert) {
304+
this.reinit({
305+
items: [options],
306+
});
307+
308+
const $stepText = this.getStepByIndex(0).find(`.${STEP_CONTENT_CLASS}`).children().eq(0);
309+
310+
assert.strictEqual($stepText.children().length, 1);
311+
assert.strictEqual($stepText.children().eq(0).hasClass(STEP_TEXT_CLASS), false);
312+
assert.strictEqual($stepText.children().eq(0).hasClass(ICON_CLASS), true);
313+
assert.strictEqual($stepText.children().eq(0).hasClass(`${ICON_CLASS}-${expectedIcon}`), true);
314+
});
315+
316+
QUnit.test(`Step indicator should be updated to '${expectedIcon}' icon after update step.isValid option at runtime`, function(assert) {
317+
this.reinit({
318+
items: [{
319+
icon: 'test',
320+
text: 'Step 1',
321+
title: 'Step title',
322+
}],
323+
});
324+
325+
this.instance.option('items[0].isValid', options.isValid);
326+
327+
const $stepText = this.getStepByIndex(0).find(`.${STEP_CONTENT_CLASS}`).children().eq(0);
328+
329+
assert.strictEqual($stepText.children().length, 1);
330+
assert.strictEqual($stepText.children().eq(0).hasClass(STEP_TEXT_CLASS), false);
331+
assert.strictEqual($stepText.children().eq(0).hasClass(ICON_CLASS), true);
332+
assert.strictEqual($stepText.children().eq(0).hasClass(`${ICON_CLASS}-${expectedIcon}`), true);
333+
});
334+
});
335+
295336
});

0 commit comments

Comments
 (0)