@configuratorware/configurator-frontendgui 1.57.13 → 1.57.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App/Modules/Designer/Components/DesignerSelectionToolbox/DesignerSelectionToolbox.js +5 -1
- package/App/Modules/Designer/Containers/DesignerSelectionToolbox.js +10 -2
- package/App/Reducers/Configurator/Reducer.js +35 -14
- package/App/Reducers/DesignArea/Selectors.js +39 -7
- package/App/Reducers/Fonts/Selectors.js +35 -10
- package/App/Services/DesignerService.js +23 -5
- package/App/Shared/Components/AmountInput/index.js +65 -44
- package/App/Shared/Components/ProductVariant/index.js +1 -1
- package/package.json +4 -4
- package/src/App/Modules/Designer/Components/DesignerSelectionToolbox/DesignerSelectionToolbox.js +4 -0
- package/src/App/Modules/Designer/Containers/DesignerSelectionToolbox.js +5 -1
- package/src/App/Reducers/Configurator/Reducer.js +67 -17
- package/src/App/Reducers/Configurator/__tests__/Reducer.test.js +230 -30
- package/src/App/Reducers/DesignArea/Selectors.js +26 -0
- package/src/App/Reducers/DesignArea/__tests__/Selectors.test.js +19 -0
- package/src/App/Reducers/Fonts/Selectors.js +25 -5
- package/src/App/Reducers/Fonts/__tests__/Selectors.test.js +14 -1
- package/src/App/Services/DesignerService.js +28 -9
- package/src/App/Services/__tests__/DesignerService.test.js +111 -0
- package/src/App/Shared/Components/AmountInput/__tests__/AmountInput.blur.test.js +249 -0
- package/src/App/Shared/Components/AmountInput/__tests__/AmountInput.retyp.test.js +57 -0
- package/src/App/Shared/Components/AmountInput/__tests__/index.test.js +16 -0
- package/src/App/Shared/Components/AmountInput/index.js +42 -38
- package/src/App/Shared/Components/ProductVariant/index.js +1 -1
|
@@ -176,6 +176,117 @@ describe('Services/DesignerService', () => {
|
|
|
176
176
|
);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
test('createColorizedTextContent uses styleColors when provided', () => {
|
|
180
|
+
const storeService = new (class StoreService {
|
|
181
|
+
static serviceName = 'StoreService';
|
|
182
|
+
state = {
|
|
183
|
+
fonts: [
|
|
184
|
+
{
|
|
185
|
+
name: 'Lobster',
|
|
186
|
+
family: 'Lobster',
|
|
187
|
+
isDefault: true,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
designArea: { selectedDesignArea: {}, filteredDesignAreas: [] },
|
|
191
|
+
configurator: { configuration: { designdata: {} } },
|
|
192
|
+
};
|
|
193
|
+
dispatch = jest.fn();
|
|
194
|
+
})();
|
|
195
|
+
ServiceLocator.provide(storeService);
|
|
196
|
+
ServiceLocator.provide(new DesignDataService());
|
|
197
|
+
|
|
198
|
+
const service = new DesignerService();
|
|
199
|
+
const colorData = {
|
|
200
|
+
identifier: '{"color":"blue","palette":"default"}',
|
|
201
|
+
value: '#0078b8',
|
|
202
|
+
};
|
|
203
|
+
const text = service.createColorizedTextContent('bulknames', 1, {
|
|
204
|
+
withPlaceholder: false,
|
|
205
|
+
styleColors: [colorData],
|
|
206
|
+
});
|
|
207
|
+
expect(text).toContain('color: #0078b8');
|
|
208
|
+
expect(text).toContain('data-color=\'{"color":"blue","palette":"default"}\'');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('createColorizedTextContent uses toolbar font selection when provided', () => {
|
|
212
|
+
const storeService = new (class StoreService {
|
|
213
|
+
static serviceName = 'StoreService';
|
|
214
|
+
state = {
|
|
215
|
+
fonts: [
|
|
216
|
+
{
|
|
217
|
+
name: 'Lobster',
|
|
218
|
+
family: 'Lobster',
|
|
219
|
+
isDefault: true,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
designArea: { selectedDesignArea: {}, filteredDesignAreas: [] },
|
|
223
|
+
configurator: { configuration: { designdata: {} } },
|
|
224
|
+
};
|
|
225
|
+
dispatch = jest.fn();
|
|
226
|
+
})();
|
|
227
|
+
ServiceLocator.provide(storeService);
|
|
228
|
+
ServiceLocator.provide(new DesignDataService());
|
|
229
|
+
|
|
230
|
+
const service = new DesignerService();
|
|
231
|
+
const text = service.createColorizedTextContent('bulknames', 1, {
|
|
232
|
+
withPlaceholder: false,
|
|
233
|
+
fontItem: { family: 'Courier New' },
|
|
234
|
+
isBold: true,
|
|
235
|
+
isItalic: true,
|
|
236
|
+
});
|
|
237
|
+
expect(text).toContain('font-family: "Courier New";');
|
|
238
|
+
expect(text).toContain('font-weight: bold;');
|
|
239
|
+
expect(text).toContain('font-style: italic;');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('bulk name toggle on and off keeps the same color and font styling', () => {
|
|
243
|
+
const storeService = new (class StoreService {
|
|
244
|
+
static serviceName = 'StoreService';
|
|
245
|
+
state = {
|
|
246
|
+
fonts: [
|
|
247
|
+
{
|
|
248
|
+
name: 'Lobster',
|
|
249
|
+
family: 'Lobster',
|
|
250
|
+
isDefault: true,
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
designArea: { selectedDesignArea: {}, filteredDesignAreas: [] },
|
|
254
|
+
configurator: { configuration: { designdata: {} } },
|
|
255
|
+
};
|
|
256
|
+
dispatch = jest.fn();
|
|
257
|
+
})();
|
|
258
|
+
ServiceLocator.provide(storeService);
|
|
259
|
+
ServiceLocator.provide(new DesignDataService());
|
|
260
|
+
|
|
261
|
+
const service = new DesignerService();
|
|
262
|
+
const styleColors = [
|
|
263
|
+
{
|
|
264
|
+
identifier: '{"color":"blue","palette":"default"}',
|
|
265
|
+
value: '#0078b8',
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
const fontOptions = {
|
|
269
|
+
styleColors,
|
|
270
|
+
fontItem: { family: 'Georgia' },
|
|
271
|
+
isBold: true,
|
|
272
|
+
isItalic: false,
|
|
273
|
+
};
|
|
274
|
+
const bulkOn = service.createColorizedTextContent('Name1,Name2', 1, {
|
|
275
|
+
withPlaceholder: false,
|
|
276
|
+
...fontOptions,
|
|
277
|
+
});
|
|
278
|
+
const bulkOff = service.createColorizedTextContent('Replace me', 1, {
|
|
279
|
+
withPlaceholder: true,
|
|
280
|
+
...fontOptions,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const sharedStyle = 'color: #0078b8; font-family: "Georgia"; font-weight: bold; font-style: normal;';
|
|
284
|
+
expect(bulkOn).toContain(sharedStyle);
|
|
285
|
+
expect(bulkOff).toContain(sharedStyle);
|
|
286
|
+
expect(bulkOff).toContain('data-placeholder="true"');
|
|
287
|
+
expect(bulkOn).not.toContain('data-placeholder="true"');
|
|
288
|
+
});
|
|
289
|
+
|
|
179
290
|
describe('changeTextFontItem', () => {
|
|
180
291
|
const createTextMock = (overrides = {}) => ({
|
|
181
292
|
type: 'Text',
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import AmountInput from 'App/Shared/Components/AmountInput';
|
|
4
|
+
import ProductVariant from 'App/Shared/Components/ProductVariant';
|
|
5
|
+
|
|
6
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
7
|
+
|
|
8
|
+
// records every value the input renders, to detect a visible revert ("flash")
|
|
9
|
+
const recordRenders = wrapper => {
|
|
10
|
+
const instance = wrapper
|
|
11
|
+
.find('AmountInput')
|
|
12
|
+
.first()
|
|
13
|
+
.instance();
|
|
14
|
+
const values = [];
|
|
15
|
+
const render = instance.render.bind(instance);
|
|
16
|
+
instance.render = () => {
|
|
17
|
+
values.push(instance.state.value);
|
|
18
|
+
return render();
|
|
19
|
+
};
|
|
20
|
+
return values;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// mimics Shared/Containers/ProductVariant: amounts live in the parent ("redux"),
|
|
24
|
+
// handleChange -> setSelectedAmount, handleMultipleAmountInputsChange -> setSelectedAmounts
|
|
25
|
+
class Harness extends React.Component {
|
|
26
|
+
state = { amounts: this.props.initialAmounts };
|
|
27
|
+
calls = [];
|
|
28
|
+
handleChange = (variant, size, value) => {
|
|
29
|
+
this.calls.push(['handleChange', (size || variant).identifier, value]);
|
|
30
|
+
this.setState(s => ({ amounts: { ...s.amounts, [(size || variant).identifier]: +value } }));
|
|
31
|
+
};
|
|
32
|
+
handleMultipleAmountInputsChange = (variant, sizes) => {
|
|
33
|
+
this.calls.push(['confirm', sizes]);
|
|
34
|
+
this.setState(s => ({ amounts: { ...s.amounts, ...sizes } }));
|
|
35
|
+
};
|
|
36
|
+
render() {
|
|
37
|
+
const { amounts } = this.state;
|
|
38
|
+
const amountData = this.props.variants.map(v => ({
|
|
39
|
+
...v,
|
|
40
|
+
amount: amounts[v.identifier] || 0,
|
|
41
|
+
sizes: v.sizes.map(size => ({ ...size, amount: amounts[size.identifier] || 0 })),
|
|
42
|
+
}));
|
|
43
|
+
return (
|
|
44
|
+
<ProductVariant
|
|
45
|
+
amountData={amountData}
|
|
46
|
+
handleChange={this.handleChange}
|
|
47
|
+
handleMultipleAmountInputsChange={this.handleMultipleAmountInputsChange}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const singleVariant = [{ identifier: 'mug', title: 'mug', url: '', sizes: [] }];
|
|
54
|
+
const multiSizeVariant = [
|
|
55
|
+
{
|
|
56
|
+
identifier: 'shirt_red',
|
|
57
|
+
title: 'red',
|
|
58
|
+
url: '',
|
|
59
|
+
sizes: [
|
|
60
|
+
{ identifier: 'shirt_red_s', size: 'Größe S', label: 'Größe S' },
|
|
61
|
+
{ identifier: 'shirt_red_m', size: 'Größe M', label: 'Größe M' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const typeAndBlur = async (input, typed) => {
|
|
67
|
+
input().simulate('focus');
|
|
68
|
+
if (typed !== undefined) {
|
|
69
|
+
input().simulate('change', { target: { value: typed } });
|
|
70
|
+
await sleep(20);
|
|
71
|
+
}
|
|
72
|
+
input().simulate('blur', { target: { value: typed } });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe('ProductVariant, variant without sizes (amount input with value label)', () => {
|
|
76
|
+
const setup = () => {
|
|
77
|
+
const wrapper = mount(<Harness variants={singleVariant} initialAmounts={{ mug: 5 }} />);
|
|
78
|
+
const harness = wrapper.find(Harness).instance();
|
|
79
|
+
const input = () => wrapper.find('input').first();
|
|
80
|
+
return { wrapper, harness, input };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
test('L1 focus + blur without editing keeps the label and sends no change', async () => {
|
|
84
|
+
const { wrapper, harness, input } = setup();
|
|
85
|
+
expect(input().prop('value')).toMatch(/^5 /);
|
|
86
|
+
await typeAndBlur(input);
|
|
87
|
+
wrapper.update();
|
|
88
|
+
expect(input().prop('value')).toMatch(/^5 /);
|
|
89
|
+
expect(harness.calls).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('L2 editing shows the new amount with label after blur', async () => {
|
|
93
|
+
const { wrapper, harness, input } = setup();
|
|
94
|
+
await typeAndBlur(input, '6');
|
|
95
|
+
wrapper.update();
|
|
96
|
+
expect(input().prop('value')).toMatch(/^6 /);
|
|
97
|
+
expect(harness.state.amounts).toEqual({ mug: 6 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('L3 editing sends the amount exactly once', async () => {
|
|
101
|
+
const { harness, input } = setup();
|
|
102
|
+
await typeAndBlur(input, '6');
|
|
103
|
+
expect(harness.calls).toEqual([['handleChange', 'mug', 6]]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('ProductVariant, variant with several sizes (amounts confirmed via button)', () => {
|
|
108
|
+
const setup = () => {
|
|
109
|
+
const wrapper = mount(<Harness variants={multiSizeVariant} initialAmounts={{}} />);
|
|
110
|
+
wrapper
|
|
111
|
+
.find('button')
|
|
112
|
+
.first()
|
|
113
|
+
.simulate('click'); // "choose size"
|
|
114
|
+
const harness = wrapper.find(Harness).instance();
|
|
115
|
+
const input = () => wrapper.find('input').first();
|
|
116
|
+
return { wrapper, harness, input };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
test('M1 typed size amount stays visible after blur', async () => {
|
|
120
|
+
const { wrapper, input } = setup();
|
|
121
|
+
const renders = recordRenders(wrapper);
|
|
122
|
+
await typeAndBlur(input, '3');
|
|
123
|
+
wrapper.update();
|
|
124
|
+
expect(input().prop('value')).toBe(3);
|
|
125
|
+
expect(renders.slice(renders.indexOf(3))).not.toContain(0); // no flash back to the old amount
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('M2 refocus + blur without editing keeps the typed amount', async () => {
|
|
129
|
+
const { wrapper, input } = setup();
|
|
130
|
+
await typeAndBlur(input, '3');
|
|
131
|
+
await typeAndBlur(input);
|
|
132
|
+
wrapper.update();
|
|
133
|
+
expect(input().prop('value')).toBe(3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('M3 confirm commits the typed amount', async () => {
|
|
137
|
+
const { wrapper, harness, input } = setup();
|
|
138
|
+
await typeAndBlur(input, '3');
|
|
139
|
+
wrapper
|
|
140
|
+
.find('button')
|
|
141
|
+
.last()
|
|
142
|
+
.simulate('click'); // "confirm selection"
|
|
143
|
+
expect(harness.state.amounts).toEqual({ shirt_red_s: 3 });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('AmountInput with debounce (CalculationWidget / AmountSelection)', () => {
|
|
148
|
+
class Controlled extends React.Component {
|
|
149
|
+
state = { value: 5 };
|
|
150
|
+
calls = [];
|
|
151
|
+
render() {
|
|
152
|
+
return (
|
|
153
|
+
<AmountInput
|
|
154
|
+
value={this.state.value}
|
|
155
|
+
onChangeDebounceWait={400}
|
|
156
|
+
onChange={value => {
|
|
157
|
+
this.calls.push(value);
|
|
158
|
+
this.setState({ value });
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const setup = () => {
|
|
165
|
+
const wrapper = mount(<Controlled />);
|
|
166
|
+
const parent = wrapper.find(Controlled).instance();
|
|
167
|
+
const input = () => wrapper.find('input');
|
|
168
|
+
return { wrapper, parent, input };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
test('D1 quick blur commits typed value once, without flashing the old value', async () => {
|
|
172
|
+
const { wrapper, parent, input } = setup();
|
|
173
|
+
const renders = recordRenders(wrapper);
|
|
174
|
+
input().simulate('focus');
|
|
175
|
+
input().simulate('change', { target: { value: '50' } });
|
|
176
|
+
input().simulate('blur', { target: { value: '50' } });
|
|
177
|
+
await sleep(500);
|
|
178
|
+
wrapper.update();
|
|
179
|
+
expect(parent.calls).toEqual([50]);
|
|
180
|
+
expect(input().prop('value')).toBe(50);
|
|
181
|
+
expect(renders.slice(renders.indexOf(50))).not.toContain(5);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('D2 blur after debounce fired does not send a duplicate', async () => {
|
|
185
|
+
const { parent, input } = setup();
|
|
186
|
+
input().simulate('focus');
|
|
187
|
+
input().simulate('change', { target: { value: '50' } });
|
|
188
|
+
await sleep(500);
|
|
189
|
+
input().simulate('blur', { target: { value: '50' } });
|
|
190
|
+
await sleep(500);
|
|
191
|
+
expect(parent.calls).toEqual([50]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('D3 focus + blur without editing sends nothing', async () => {
|
|
195
|
+
const { parent, input } = setup();
|
|
196
|
+
input().simulate('focus');
|
|
197
|
+
input().simulate('blur', { target: { value: '5' } });
|
|
198
|
+
await sleep(500);
|
|
199
|
+
expect(parent.calls).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('D4 emptied field falls back to min on blur', async () => {
|
|
203
|
+
const { wrapper, parent, input } = setup();
|
|
204
|
+
input().simulate('focus');
|
|
205
|
+
input().simulate('change', { target: { value: '' } });
|
|
206
|
+
input().simulate('blur', { target: { value: '' } });
|
|
207
|
+
await sleep(500);
|
|
208
|
+
wrapper.update();
|
|
209
|
+
expect(parent.calls[parent.calls.length - 1]).toBe(0);
|
|
210
|
+
expect(input().prop('value')).toBe(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('D5 values above max are clamped', async () => {
|
|
214
|
+
const { wrapper, parent, input } = setup();
|
|
215
|
+
input().simulate('focus');
|
|
216
|
+
input().simulate('change', { target: { value: '99999999' } });
|
|
217
|
+
input().simulate('blur', { target: { value: '99999999' } });
|
|
218
|
+
await sleep(500);
|
|
219
|
+
wrapper.update();
|
|
220
|
+
expect(parent.calls).toEqual([999999]);
|
|
221
|
+
expect(input().prop('value')).toBe(999999);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('D6 readOnly input picks up prop changes while focused', () => {
|
|
225
|
+
const wrapper = mount(<AmountInput value={5} readOnly onChange={jest.fn()} />);
|
|
226
|
+
wrapper.find('input').simulate('focus');
|
|
227
|
+
wrapper.setProps({ value: 8 });
|
|
228
|
+
wrapper.update();
|
|
229
|
+
expect(wrapper.find('input').prop('value')).toBe(8);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('ProductVariant, clearing an amount to retype it', () => {
|
|
233
|
+
it('never sends 0 for a variant without sizes', async () => {
|
|
234
|
+
const wrapper = mount(<Harness variants={singleVariant} initialAmounts={{ mug: 5 }} />);
|
|
235
|
+
const harness = wrapper.find(Harness).instance();
|
|
236
|
+
const input = () => wrapper.find('input').first();
|
|
237
|
+
|
|
238
|
+
input().simulate('focus');
|
|
239
|
+
input().simulate('change', { target: { value: '' } });
|
|
240
|
+
await sleep(20);
|
|
241
|
+
input().simulate('change', { target: { value: '6' } });
|
|
242
|
+
await sleep(20);
|
|
243
|
+
input().simulate('blur');
|
|
244
|
+
|
|
245
|
+
expect(harness.calls).not.toContainEqual(['handleChange', 'mug', 0]);
|
|
246
|
+
expect(harness.calls).toContainEqual(['handleChange', 'mug', 6]);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import AmountInput from 'App/Shared/Components/AmountInput';
|
|
4
|
+
|
|
5
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
6
|
+
|
|
7
|
+
describe('AmountInput, clearing the field to retype', () => {
|
|
8
|
+
const setup = () => {
|
|
9
|
+
const onChange = jest.fn();
|
|
10
|
+
const wrapper = mount(<AmountInput value={2} onChange={onChange} onChangeDebounceWait={10} />);
|
|
11
|
+
const input = () => wrapper.find('input').first();
|
|
12
|
+
input().simulate('focus');
|
|
13
|
+
return { wrapper, onChange, input };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
it('does not send 0 while the field is empty and the user retypes', async () => {
|
|
17
|
+
const { onChange, input } = setup();
|
|
18
|
+
|
|
19
|
+
input().simulate('change', { target: { value: '' } });
|
|
20
|
+
await sleep(30);
|
|
21
|
+
input().simulate('change', { target: { value: '3' } });
|
|
22
|
+
await sleep(30);
|
|
23
|
+
input().simulate('blur');
|
|
24
|
+
|
|
25
|
+
expect(onChange).not.toHaveBeenCalledWith(0);
|
|
26
|
+
expect(onChange).toHaveBeenCalledWith(3);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('commits 0 on blur when the field stays empty', async () => {
|
|
30
|
+
const { onChange, input } = setup();
|
|
31
|
+
|
|
32
|
+
input().simulate('change', { target: { value: '' } });
|
|
33
|
+
await sleep(30);
|
|
34
|
+
input().simulate('blur');
|
|
35
|
+
|
|
36
|
+
expect(onChange.mock.calls).toEqual([[0]]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('commits 0 on unmount when the field is left empty', async () => {
|
|
40
|
+
const { wrapper, onChange, input } = setup();
|
|
41
|
+
|
|
42
|
+
input().simulate('change', { target: { value: '' } });
|
|
43
|
+
await sleep(30);
|
|
44
|
+
wrapper.unmount();
|
|
45
|
+
|
|
46
|
+
expect(onChange.mock.calls).toEqual([[0]]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('still sends a typed 0 right away', async () => {
|
|
50
|
+
const { onChange, input } = setup();
|
|
51
|
+
|
|
52
|
+
input().simulate('change', { target: { value: '0' } });
|
|
53
|
+
await sleep(30);
|
|
54
|
+
|
|
55
|
+
expect(onChange).toHaveBeenCalledWith(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import AmountInput from '../index';
|
|
4
|
+
|
|
5
|
+
describe('Shared/Components/AmountInput', () => {
|
|
6
|
+
test('typed value survives blur before debounce', async () => {
|
|
7
|
+
const onChange = jest.fn();
|
|
8
|
+
const wrapper = mount(<AmountInput value={5} onChange={onChange} onChangeDebounceWait={400} />);
|
|
9
|
+
const input = wrapper.find('input');
|
|
10
|
+
input.simulate('focus');
|
|
11
|
+
input.simulate('change', { target: { value: '50' } });
|
|
12
|
+
input.simulate('blur', { target: { value: '50' } });
|
|
13
|
+
await new Promise(r => setTimeout(r, 500));
|
|
14
|
+
expect(onChange).toHaveBeenCalledWith(50);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -58,7 +58,7 @@ const styles = theme => ({
|
|
|
58
58
|
|
|
59
59
|
class AmountInput extends React.Component {
|
|
60
60
|
static propTypes = {
|
|
61
|
-
value: PropTypes.string,
|
|
61
|
+
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
62
62
|
classes: PropTypes.object,
|
|
63
63
|
inputProps: PropTypes.object,
|
|
64
64
|
rootProps: PropTypes.string,
|
|
@@ -91,29 +91,22 @@ class AmountInput extends React.Component {
|
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
componentDidMount() {
|
|
94
|
-
|
|
95
|
-
props: { value },
|
|
96
|
-
} = this;
|
|
97
|
-
this.setState({ value });
|
|
94
|
+
this.setState({ value: this.props.value });
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
componentDidUpdate(prevProps) {
|
|
101
|
-
const {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (prevProps.value !== value && !this.state.changeInprogress) {
|
|
105
|
-
this.updateDebounced();
|
|
98
|
+
const { value, readOnly } = this.props;
|
|
99
|
+
if (prevProps.value !== value && (readOnly || !this.state.changeInprogress)) {
|
|
100
|
+
this.setState({ value });
|
|
106
101
|
}
|
|
107
102
|
}
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (value !== this.state.value) {
|
|
114
|
-
this.setState({ value });
|
|
104
|
+
componentWillUnmount() {
|
|
105
|
+
this.onChangeDebounced.cancel();
|
|
106
|
+
if (this.hasPendingChange) {
|
|
107
|
+
this.props.onChange(this.state.value === '' ? this.props.min : this.state.value);
|
|
115
108
|
}
|
|
116
|
-
}
|
|
109
|
+
}
|
|
117
110
|
|
|
118
111
|
validateKey = event => {
|
|
119
112
|
const regEx = /^[0-9\b]+$/;
|
|
@@ -124,41 +117,52 @@ class AmountInput extends React.Component {
|
|
|
124
117
|
}
|
|
125
118
|
};
|
|
126
119
|
|
|
120
|
+
hasPendingChange = false;
|
|
121
|
+
|
|
122
|
+
// An empty field means the user is retyping: committing 0 here would drop data tied to the amount
|
|
123
|
+
// (e.g. bulk names). It is committed on blur or unmount instead.
|
|
127
124
|
onChangeDebounced = debounce(() => {
|
|
128
|
-
|
|
125
|
+
if (this.state.value === '') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.hasPendingChange = false;
|
|
129
|
+
this.props.onChange(this.state.value);
|
|
129
130
|
}, this.props.onChangeDebounceWait);
|
|
130
131
|
|
|
131
132
|
onChange = ({ target: { value: inputValue } }) => {
|
|
132
133
|
const { min, max } = this.props;
|
|
133
|
-
const value = !inputValue ? inputValue : Math.min(max, Math.max(min, parseInt(inputValue)));
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
value: isNaN(value) ? '' : value,
|
|
137
|
-
},
|
|
138
|
-
this.onChangeDebounced
|
|
139
|
-
);
|
|
134
|
+
const value = !inputValue ? inputValue : Math.min(max, Math.max(min, parseInt(inputValue, 10)));
|
|
135
|
+
this.hasPendingChange = true;
|
|
136
|
+
this.setState({ value: Number.isNaN(value) ? '' : value }, this.onChangeDebounced);
|
|
140
137
|
};
|
|
141
138
|
|
|
142
139
|
onBlur = event => {
|
|
143
|
-
const {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const initialValue = min;
|
|
150
|
-
this.setState({ value: initialValue });
|
|
151
|
-
onChange(initialValue);
|
|
140
|
+
const { onChange, onBlur, min, readOnly, hasValueLabel } = this.props;
|
|
141
|
+
|
|
142
|
+
if (readOnly) {
|
|
143
|
+
this.setState({ changeInprogress: false, value: this.props.value });
|
|
144
|
+
onBlur && onBlur(event);
|
|
145
|
+
return;
|
|
152
146
|
}
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
|
|
148
|
+
this.onChangeDebounced.cancel();
|
|
149
|
+
const isCleared = this.state.value === '';
|
|
150
|
+
const shouldCommit = this.hasPendingChange || isCleared;
|
|
151
|
+
const committedValue = isCleared ? min : this.state.value;
|
|
152
|
+
this.hasPendingChange = false;
|
|
153
|
+
|
|
154
|
+
this.setState({ changeInprogress: false, value: hasValueLabel ? this.props.value : committedValue });
|
|
155
|
+
if (shouldCommit) {
|
|
156
|
+
onChange(committedValue);
|
|
155
157
|
}
|
|
156
158
|
onBlur && onBlur(event);
|
|
157
159
|
};
|
|
158
160
|
|
|
159
161
|
onFocus = event => {
|
|
160
|
-
const { onFocus } = this.props;
|
|
161
|
-
|
|
162
|
+
const { onFocus, readOnly } = this.props;
|
|
163
|
+
if (!readOnly) {
|
|
164
|
+
this.setState({ changeInprogress: true });
|
|
165
|
+
}
|
|
162
166
|
onFocus && onFocus(event);
|
|
163
167
|
};
|
|
164
168
|
|
|
@@ -374,8 +374,8 @@ class ProductVariant extends React.Component {
|
|
|
374
374
|
{get(variant, 'sizes.length', 0) === 0 && (
|
|
375
375
|
<AmountInput
|
|
376
376
|
value={variant.amount + ' ' + t('amountSelection.amount')}
|
|
377
|
-
hasValueLabel={true}
|
|
378
377
|
amountLabel={t('productVariant.amountLabel')}
|
|
378
|
+
hasValueLabel={true}
|
|
379
379
|
onChange={this.handleChange(variant)}
|
|
380
380
|
onClick={this.handleBulkNameForm(variant)}
|
|
381
381
|
error={hasError(variant.identifier, error)}
|