Skip to content

Commit 1c2c5b5

Browse files
authored
NumberBox: keep the format prefix visible when navigating to the start boundary (T1330133) (#33937)
1 parent 0f415d7 commit 1c2c5b5

2 files changed

Lines changed: 175 additions & 12 deletions

File tree

packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ import {
2626
} from './m_utils';
2727

2828
const NUMBER_FORMATTER_NAMESPACE = 'dxNumberFormatter';
29-
const MOVE_FORWARD = 1;
30-
const MOVE_BACKWARD = -1;
29+
const MOVE_FORWARD = 1 as const;
30+
const MOVE_BACKWARD = -1 as const;
3131
const MINUS = '-';
3232
const MINUS_KEY = 'minus';
3333
const INPUT_EVENT = 'input';
3434
const NUMPAD_DOT_KEY_CODE = 110;
3535

3636
const CARET_TIMEOUT_DURATION = 0;
3737

38+
type CaretMoveDirection = typeof MOVE_FORWARD | typeof MOVE_BACKWARD;
39+
3840
export interface NumberBoxMaskProperties extends Omit<Properties, 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput'
3941
| 'onKeyDown' | 'onKeyUp' | 'onPaste' | 'onValueChanged' | 'onContentReady' | 'onDisposing'
4042
| 'onOptionChanged' | 'onInitialized' > {
@@ -87,9 +89,9 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> {
8789
backspace: that._removeHandler.bind(that),
8890
leftArrow: that._arrowHandler.bind(that, MOVE_BACKWARD),
8991
rightArrow: that._arrowHandler.bind(that, MOVE_FORWARD),
90-
home: that._moveCaretToBoundaryEventHandler.bind(that, MOVE_FORWARD),
92+
home: that._boundaryKeyHandler.bind(that, MOVE_FORWARD),
9193
enter: that._updateFormattedValue.bind(that),
92-
end: that._moveCaretToBoundaryEventHandler.bind(that, MOVE_BACKWARD),
94+
end: that._boundaryKeyHandler.bind(that, MOVE_BACKWARD),
9395
};
9496
}
9597

@@ -122,7 +124,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> {
122124
if (decimalSeparatorIndex >= 0) {
123125
this._caret({ start: decimalSeparatorIndex, end: decimalSeparatorIndex });
124126
} else {
125-
this._moveCaretToBoundaryEventHandler(MOVE_BACKWARD, e);
127+
this._moveCaretToBoundary(MOVE_BACKWARD);
126128
}
127129
}
128130
}, CARET_TIMEOUT_DURATION);
@@ -187,23 +189,42 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> {
187189
nextCaret = step === MOVE_FORWARD ? nextCaret.end : nextCaret.start;
188190
e.preventDefault();
189191
this._caret(getCaretInBoundaries(nextCaret, text, format));
192+
this._scrollInputTo(step === MOVE_FORWARD ? 'end' : 'start');
193+
}
194+
}
195+
196+
_scrollInputTo(edge: 'start' | 'end'): void {
197+
const inputElement = this._input().get(0);
198+
if (!inputElement) {
199+
return;
190200
}
201+
inputElement.scrollLeft = edge === 'end' ? inputElement.scrollWidth : 0;
191202
}
192203

193-
_moveCaretToBoundary(direction) {
194-
const boundaries = getCaretBoundaries(this._getInputVal(), this._getFormatPattern());
195-
const newCaret = getCaretWithOffset(direction === MOVE_FORWARD ? boundaries.start : boundaries.end, 0);
204+
_moveCaretToBoundary(direction: CaretMoveDirection): void {
205+
const boundaries = getCaretBoundaries(
206+
this._getInputVal(),
207+
this._getFormatPattern(),
208+
);
209+
210+
const newCaret = getCaretWithOffset(
211+
direction === MOVE_FORWARD
212+
? boundaries.start
213+
: boundaries.end,
214+
0,
215+
);
196216

197217
this._caret(newCaret);
198218
}
199219

200-
_moveCaretToBoundaryEventHandler(direction, e) {
201-
if (!this._useMaskBehavior() || e?.shiftKey) {
220+
_boundaryKeyHandler(direction: CaretMoveDirection, e: KeyboardEvent): void {
221+
if (!this._useMaskBehavior() || e.shiftKey) {
202222
return;
203223
}
204224

205225
this._moveCaretToBoundary(direction);
206-
e?.preventDefault();
226+
e.preventDefault();
227+
this._scrollInputTo(direction === MOVE_FORWARD ? 'start' : 'end');
207228
}
208229

209230
_shouldMoveCaret(text, caret) {
@@ -769,10 +790,11 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> {
769790
const caret = this._caret();
770791
const textWithoutMinus = this._removeMinusFromText(normalizedText, caret);
771792
const wasMinusRemoved = textWithoutMinus !== normalizedText;
793+
const isFromPaste = this._isInputFromPaste(e);
772794

773795
normalizedText = textWithoutMinus;
774796

775-
if (!this._isInputFromPaste(e) && this._isValueIncomplete(textWithoutMinus)) {
797+
if (!isFromPaste && this._isValueIncomplete(textWithoutMinus)) {
776798
this._formattedValue = normalizedText;
777799
if (wasMinusRemoved) {
778800
this._setTextByParsedValue();
@@ -781,8 +803,10 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> {
781803
}
782804

783805
const textWasChanged = number.convertDigits(this._formattedValue, true) !== normalizedText;
806+
784807
if (textWasChanged) {
785808
const value = this._tryParse(normalizedText, caret, '');
809+
786810
if (isDefined(value)) {
787811
this._parsedValue = value;
788812
}

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.tests.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,6 +2119,145 @@ QUnit.module('format: caret boundaries', moduleConfig, () => {
21192119
});
21202120
});
21212121

2122+
QUnit.module('format: scroll position on boundary keys (T1330133)', {
2123+
beforeEach: function() {
2124+
moduleConfig.beforeEach.call(this);
2125+
2126+
this.scrollState = { scrollWidth: 200, scrollLeft: 0 };
2127+
2128+
Object.defineProperty(this.inputElement, 'scrollWidth', {
2129+
get: () => this.scrollState.scrollWidth,
2130+
configurable: true,
2131+
});
2132+
2133+
Object.defineProperty(this.inputElement, 'scrollLeft', {
2134+
get: () => this.scrollState.scrollLeft,
2135+
set: (value) => { this.scrollState.scrollLeft = value; },
2136+
configurable: true,
2137+
});
2138+
2139+
this.assertScrolledTo = (assert, expected, message) => {
2140+
const actual = expected === 'end' ? this.scrollState.scrollWidth : 0;
2141+
assert.strictEqual(this.scrollState.scrollLeft, actual, message);
2142+
};
2143+
},
2144+
afterEach: function() {
2145+
delete this.inputElement.scrollLeft;
2146+
delete this.inputElement.scrollWidth;
2147+
moduleConfig.afterEach.call(this);
2148+
}
2149+
}, () => {
2150+
QUnit.module('arrow keys', () => {
2151+
QUnit.test('left arrow at the start boundary scrolls the input to the start edge so the minus sign becomes visible (T1330133)', function(assert) {
2152+
this.instance.option({
2153+
format: '#,##0.00',
2154+
value: -4589999.89,
2155+
});
2156+
2157+
this.scrollState.scrollLeft = 50;
2158+
this.keyboard.caret(1).keyDown('left');
2159+
2160+
assert.ok(this.keyboard.event.isDefaultPrevented(), 'default is still prevented');
2161+
assert.deepEqual(this.keyboard.caret(), { start: 1, end: 1 }, 'caret stays on the start boundary');
2162+
this.assertScrolledTo(assert, 'start', 'input is scrolled to the start edge');
2163+
});
2164+
2165+
QUnit.test('left arrow at the prefix-stub boundary scrolls the input to the start edge', function(assert) {
2166+
this.instance.option({
2167+
format: '$#',
2168+
value: 1,
2169+
});
2170+
2171+
this.scrollState.scrollLeft = 50;
2172+
this.keyboard.caret(1).keyDown('left');
2173+
2174+
assert.ok(this.keyboard.event.isDefaultPrevented(), 'default is still prevented');
2175+
this.assertScrolledTo(assert, 'start', 'input is scrolled so the prefix stub becomes visible');
2176+
});
2177+
2178+
QUnit.test('right arrow at the end boundary scrolls the input to the end edge', function(assert) {
2179+
this.instance.option({
2180+
format: '#d',
2181+
value: 1,
2182+
});
2183+
2184+
this.scrollState.scrollLeft = 0;
2185+
this.keyboard.caret(1).keyDown('right');
2186+
2187+
assert.ok(this.keyboard.event.isDefaultPrevented(), 'default is still prevented');
2188+
this.assertScrolledTo(assert, 'end', 'input is scrolled to the end edge');
2189+
});
2190+
2191+
QUnit.test('arrow key within boundaries does not adjust the scroll position', function(assert) {
2192+
this.instance.option({
2193+
format: '#,##0.00',
2194+
value: -4589999.89
2195+
});
2196+
2197+
this.scrollState.scrollLeft = 42;
2198+
this.keyboard.caret(5).keyDown('left');
2199+
2200+
assert.notOk(this.keyboard.event.isDefaultPrevented(), 'default is not prevented inside the boundaries');
2201+
assert.strictEqual(this.scrollState.scrollLeft, 42, 'scroll position is not adjusted');
2202+
});
2203+
});
2204+
2205+
QUnit.module('Home, End keys', () => {
2206+
QUnit.test('Home scrolls the input to the start edge so the prefix becomes visible (T1330133)', function(assert) {
2207+
this.instance.option({
2208+
format: '#,##0.00',
2209+
value: -4589999.89,
2210+
});
2211+
2212+
this.scrollState.scrollLeft = 50;
2213+
this.keyboard.caret(13).keyDown('home');
2214+
2215+
assert.deepEqual(this.keyboard.caret(), { start: 1, end: 1 }, 'caret is on the start boundary');
2216+
this.assertScrolledTo(assert, 'start', 'input is scrolled so the minus sign becomes visible');
2217+
});
2218+
2219+
QUnit.test('End scrolls the input to the end edge so the trailing digits become visible (T1330133)', function(assert) {
2220+
this.instance.option({
2221+
format: '#,##0.00',
2222+
value: -4589999.89,
2223+
});
2224+
2225+
this.scrollState.scrollLeft = 0;
2226+
this.keyboard.caret(1).keyDown('end');
2227+
2228+
assert.deepEqual(this.keyboard.caret(), { start: 13, end: 13 }, 'caret is on the end boundary');
2229+
this.assertScrolledTo(assert, 'end', 'input is scrolled so the trailing digits become visible');
2230+
});
2231+
2232+
['home', 'end'].forEach((key) => {
2233+
QUnit.test(`shift+${key} does not adjust the scroll position`, function(assert) {
2234+
this.scrollState.scrollLeft = 30;
2235+
this.keyboard.keyDown(key, { shiftKey: true });
2236+
2237+
assert.strictEqual(this.scrollState.scrollLeft, 30, 'scroll position is not adjusted');
2238+
});
2239+
});
2240+
});
2241+
2242+
QUnit.module('focus-in semantics', () => {
2243+
QUnit.test('focus on integer-only format does not force-scroll the input to a boundary', function(assert) {
2244+
this.instance.option({
2245+
format: '#,##0',
2246+
value: -4589999,
2247+
});
2248+
2249+
this.scrollState.scrollLeft = 42;
2250+
2251+
this.input.focus();
2252+
this.clock.tick(CARET_TIMEOUT_DURATION);
2253+
2254+
assert.notStrictEqual(this.scrollState.scrollLeft, this.scrollState.scrollWidth, 'scroll is not forced to the end edge on focus');
2255+
assert.notStrictEqual(this.scrollState.scrollLeft, 0, 'scroll is not forced to the start edge on focus');
2256+
assert.strictEqual(this.scrollState.scrollLeft, 42, 'scroll position is left to the browser');
2257+
});
2258+
});
2259+
});
2260+
21222261
QUnit.module('format: custom parser and formatter', moduleConfig, () => {
21232262
QUnit.test('custom parser and formatter should work', function(assert) {
21242263
this.instance.option({

0 commit comments

Comments
 (0)