From bd3d1e374a2f99d54be7dd63d9f5d622016f63e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 28 Apr 2023 12:41:11 +0200 Subject: [PATCH] =?UTF-8?q?fix(InputMasked):=20on=20custom=20mask=20?= =?UTF-8?q?=E2=80=93=20avoid=20interaction=20stall=20after=20focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input-masked/InputMaskedHooks.js | 4 +- .../input-masked/InputMaskedUtils.js | 37 +++++++++++++--- .../__tests__/InputMasked.test.tsx | 43 ++++++++++++++++++- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js b/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js index 29d20fe127c..57374a3a43f 100644 --- a/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js +++ b/packages/dnb-eufemia/src/components/input-masked/InputMaskedHooks.js @@ -414,7 +414,7 @@ const useCallEvent = ({ setLocalValue }) => { const cleanedValue = numberValue === 0 ? '' : num if (name === 'on_change' && numberValue === 0) { - correctCaretPosition(event.target, maskParams) + correctCaretPosition(event.target, maskParams, props) } const result = dispatchCustomElementEvent(props, name, { @@ -433,7 +433,7 @@ const useCallEvent = ({ setLocalValue }) => { !props.selectall ) { // Also correct here, because of additional click inside the field - correctCaretPosition(event.target, maskParams) + correctCaretPosition(event.target, maskParams, props) } return result diff --git a/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js b/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js index 4ad3cae2f76..4f39bfee388 100644 --- a/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js +++ b/packages/dnb-eufemia/src/components/input-masked/InputMaskedUtils.js @@ -176,20 +176,20 @@ export const correctNumberValue = ({ * @param {Element} element Input Element * @param {Object} maskParams Mask parameters, containing eventually suffix or prefix */ -export const correctCaretPosition = (element, maskParams) => { +export const correctCaretPosition = (element, maskParams, props) => { const correction = () => { try { const suffix = maskParams?.suffix const prefix = maskParams?.prefix - if (suffix || prefix) { - const start = element.selectionStart - const end = element.selectionEnd + const start = element.selectionStart + const end = element.selectionEnd - if (start !== end) { - return // stop here - } + if (start !== end) { + return // stop here + } + if (suffix || prefix) { const suffixStart = element.value.indexOf(suffix) const suffixEnd = suffixStart + suffix?.length let pos = undefined @@ -223,6 +223,29 @@ export const correctCaretPosition = (element, maskParams) => { if (!isNaN(parseFloat(pos))) { safeSetSelection(element, pos) } + } else if (props?.mask && element.value.length === end) { + const chars = element.value.split('') + + for (let l = chars.length, i = l - 1; i >= 0; i--) { + const char = chars[i] + const mask = props.mask[i] + if ( + char && + char !== invisibleSpace && + mask instanceof RegExp && + mask.test(char) + ) { + for (let n = i + 1; n < l; n++) { + const mask = props.mask[n] + if (mask?.test?.(mask) === false) { + safeSetSelection(element, n) + break + } + } + + break + } + } } } catch (e) { warn(e) diff --git a/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx b/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx index f1444706689..d1aadd626e5 100644 --- a/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx +++ b/packages/dnb-eufemia/src/components/input-masked/__tests__/InputMasked.test.tsx @@ -644,7 +644,7 @@ describe('InputMasked component', () => { '​ kr' // includes a hidden space: invisibleSpace ) - await wait(2) + await wait(2) // because of the delayed requestAnimationFrame expect(setSelectionRange).toBeCalledTimes(1) expect(setSelectionRange).toHaveBeenCalledWith(0, 0) @@ -665,7 +665,7 @@ describe('InputMasked component', () => { 'Prefix ​ kr' // includes a hidden space: invisibleSpace ) - await wait(2) + await wait(2) // because of the delayed requestAnimationFrame expect(setSelectionRange).toBeCalledTimes(2) expect(setSelectionRange).toHaveBeenCalledWith(8, 8) @@ -1518,6 +1518,45 @@ describe('InputMasked component as_currency', () => { 'dnb-input--vertical', ]) }) + + it('should set correct cursor position on focus and mouseUp', async () => { + render( + + ) + + const element = document.querySelector('input') + + const preventDefault = jest.fn() + element.setSelectionRange = jest.fn() + + // 1. Test first focus + fireEvent.focus(element, { + target: { + selectionStart: 6, + }, + preventDefault, + }) + + await wait(2) // because of the delayed requestAnimationFrame + + expect(element.setSelectionRange).toHaveBeenCalledTimes(1) + expect(element.setSelectionRange).toHaveBeenNthCalledWith(1, 4, 4) + + // 2. Then test mouse up + fireEvent.mouseUp(element, { + target: { + selectionStart: 6, + }, + preventDefault, + }) + + await wait(2) // because of the delayed requestAnimationFrame + + expect(element.setSelectionRange).toHaveBeenCalledTimes(2) + expect(element.setSelectionRange).toHaveBeenNthCalledWith(2, 4, 4) + + expect(element.value).toBe('12––​​') + }) }) describe('InputMasked scss', () => {