From d85983cc850dacdaac074e652a8cb3a7d859af67 Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Thu, 19 Sep 2024 11:58:10 +0300 Subject: [PATCH] fix(core): fix scroll for masked narrow textfields --- projects/core/src/lib/mask.ts | 66 +++++++++++-------- .../core/src/lib/utils/dom/event-listener.ts | 4 +- .../kit/date-range/date-range-basic.cy.ts | 4 ++ .../src/tests/kit/date/date-basic.cy.ts | 2 + .../textarea-latin-letters-digits.cy.ts | 2 + 5 files changed, 50 insertions(+), 28 deletions(-) diff --git a/projects/core/src/lib/mask.ts b/projects/core/src/lib/mask.ts index a133ccc5d..165101f74 100644 --- a/projects/core/src/lib/mask.ts +++ b/projects/core/src/lib/mask.ts @@ -27,6 +27,8 @@ export class Maskito extends MaskHistory { ...this.maskitoOptions, }; + private upcomingElementState: ElementState | null = null; + private readonly preprocessor = maskitoPipe(this.options.preprocessors); private readonly postprocessor = maskitoPipe(this.options.postprocessors); @@ -132,6 +134,17 @@ export class Maskito extends MaskHistory { } }); + this.eventListener.listen( + 'input', + () => { + if (this.upcomingElementState) { + this.updateElementState(this.upcomingElementState); + this.upcomingElementState = null; + } + }, + {capture: true}, + ); + this.eventListener.listen('input', ({inputType}) => { if (inputType === 'insertCompositionText') { return; // will be handled inside `compositionend` event @@ -154,17 +167,14 @@ export class Maskito extends MaskHistory { protected updateElementState( {value, selection}: ElementState, - eventInit: Pick = { - inputType: 'insertText', - data: null, - }, + eventInit?: Pick, ): void { const initialValue = this.elementState.value; this.updateValue(value); this.updateSelectionRange(selection); - if (initialValue !== value) { + if (eventInit && initialValue !== value) { this.dispatchInputEvent(eventInit); } } @@ -200,7 +210,10 @@ export class Maskito extends MaskHistory { } private ensureValueFitsMask(): void { - this.updateElementState(maskitoTransform(this.elementState, this.options)); + this.updateElementState(maskitoTransform(this.elementState, this.options), { + inputType: 'insertText', + data: null, + }); } private dispatchInputEvent( @@ -261,24 +274,20 @@ export class Maskito extends MaskHistory { return; } - event.preventDefault(); - if ( areElementValuesEqual(initialState, elementState, maskModel, newElementState) ) { + event.preventDefault(); + // User presses Backspace/Delete for the fixed value return this.updateSelectionRange(isForward ? [to, to] : [from, from]); } - this.updateElementState(newElementState, { - inputType: event.inputType, - data: null, - }); - this.updateHistory(newElementState); + this.upcomingElementState = newElementState; } private handleInsert(event: TypedInputEvent, data: string): void { - const initialElementState = this.elementState; + const {options, maxLength, element, elementState: initialElementState} = this; const {elementState, data: insertedText = data} = this.preprocessor( { data, @@ -286,7 +295,7 @@ export class Maskito extends MaskHistory { }, 'insert', ); - const maskModel = new MaskModel(elementState, this.options); + const maskModel = new MaskModel(elementState, options); try { maskModel.addCharacters(elementState.selection, insertedText); @@ -301,21 +310,24 @@ export class Maskito extends MaskHistory { initialElementState.value.slice(to); const newElementState = this.postprocessor(maskModel, initialElementState); - if (newElementState.value.length > this.maxLength) { + if (newElementState.value.length > maxLength) { return event.preventDefault(); } - if ( - newPossibleValue !== newElementState.value || - this.element.isContentEditable - ) { - event.preventDefault(); - - this.updateElementState(newElementState, { - data, - inputType: event.inputType, - }); - this.updateHistory(newElementState); + if (newPossibleValue !== newElementState.value || element.isContentEditable) { + this.upcomingElementState = newElementState; + + if ( + options.overwriteMode === 'replace' && + newPossibleValue.length > maxLength + ) { + /** + * Browsers know nothing about Maskito and its `overwriteMode`. + * When textfield value length is already equal to attribute `maxlength`, + * pressing any key (even with valid value) does not emit `input` event. + */ + this.dispatchInputEvent({inputType: 'insertText', data}); + } } } diff --git a/projects/core/src/lib/utils/dom/event-listener.ts b/projects/core/src/lib/utils/dom/event-listener.ts index e04ec7e2d..1892f2848 100644 --- a/projects/core/src/lib/utils/dom/event-listener.ts +++ b/projects/core/src/lib/utils/dom/event-listener.ts @@ -17,7 +17,9 @@ export class EventListener { const untypedFn = fn as (event: HTMLElementEventMap[E]) => unknown; this.element.addEventListener(eventType, untypedFn, options); - this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn)); + this.listeners.push(() => + this.element.removeEventListener(eventType, untypedFn, options), + ); } public destroy(): void { diff --git a/projects/demo-integrations/src/tests/kit/date-range/date-range-basic.cy.ts b/projects/demo-integrations/src/tests/kit/date-range/date-range-basic.cy.ts index abdaa8abe..40fa3c685 100644 --- a/projects/demo-integrations/src/tests/kit/date-range/date-range-basic.cy.ts +++ b/projects/demo-integrations/src/tests/kit/date-range/date-range-basic.cy.ts @@ -152,6 +152,7 @@ describe('DateRange | Basic', () => { it('Type `deleteSoftLineBackward` of `InputEvent` works', () => { cy.get('@input') .trigger('beforeinput', {inputType: 'deleteSoftLineBackward'}) + .trigger('input', {inputType: 'deleteSoftLineBackward'}) .should('have.value', '') .should('have.prop', 'selectionStart', ''.length) .should('have.prop', 'selectionEnd', ''.length); @@ -161,6 +162,7 @@ describe('DateRange | Basic', () => { cy.get('@input') .type('{moveToStart}') .trigger('beforeinput', {inputType: 'deleteSoftLineForward'}) + .trigger('input', {inputType: 'deleteSoftLineForward'}) .should('have.value', '') .should('have.prop', 'selectionStart', ''.length) .should('have.prop', 'selectionEnd', ''.length); @@ -170,6 +172,7 @@ describe('DateRange | Basic', () => { cy.get('@input') .type('{leftArrow}'.repeat(' 31.12.2022'.length)) .trigger('beforeinput', {inputType: 'deleteSoftLineBackward'}) + .trigger('input', {inputType: 'deleteSoftLineBackward'}) .should('have.value', '01.01.0001 – 31.12.2022') .should('have.prop', 'selectionStart', ''.length) .should('have.prop', 'selectionEnd', ''.length); @@ -179,6 +182,7 @@ describe('DateRange | Basic', () => { cy.get('@input') .type('{leftArrow}'.repeat(' 31.12.2022'.length)) .trigger('beforeinput', {inputType: 'deleteSoftLineForward'}) + .trigger('input', {inputType: 'deleteSoftLineForward'}) .should('have.value', '20.01.1990') .should('have.prop', 'selectionStart', '20.01.1990'.length) .should('have.prop', 'selectionEnd', '20.01.1990'.length); diff --git a/projects/demo-integrations/src/tests/kit/date/date-basic.cy.ts b/projects/demo-integrations/src/tests/kit/date/date-basic.cy.ts index 4f04f214c..ff7b60198 100644 --- a/projects/demo-integrations/src/tests/kit/date/date-basic.cy.ts +++ b/projects/demo-integrations/src/tests/kit/date/date-basic.cy.ts @@ -127,6 +127,7 @@ describe('Date', () => { cy.get('@input') .type('{moveToStart}') .trigger('beforeinput', {inputType: 'deleteSoftLineForward'}) + .trigger('input', {inputType: 'deleteSoftLineForward'}) .should('have.value', '') .should('have.prop', 'selectionStart', ''.length) .should('have.prop', 'selectionEnd', ''.length); @@ -135,6 +136,7 @@ describe('Date', () => { it('Type `deleteSoftLineBackward` of `InputEvent` works', () => { cy.get('@input') .trigger('beforeinput', {inputType: 'deleteSoftLineBackward'}) + .trigger('input', {inputType: 'deleteSoftLineBackward'}) .should('have.value', '') .should('have.prop', 'selectionStart', ''.length) .should('have.prop', 'selectionEnd', ''.length); diff --git a/projects/demo-integrations/src/tests/recipes/textarea/textarea-latin-letters-digits.cy.ts b/projects/demo-integrations/src/tests/recipes/textarea/textarea-latin-letters-digits.cy.ts index c9695a431..d4b790a52 100644 --- a/projects/demo-integrations/src/tests/recipes/textarea/textarea-latin-letters-digits.cy.ts +++ b/projects/demo-integrations/src/tests/recipes/textarea/textarea-latin-letters-digits.cy.ts @@ -48,6 +48,7 @@ describe('Textarea (mask latin letters + digits)', () => { .type('{enter}') .type('UI and Maskito') .trigger('beforeinput', {inputType: 'deleteSoftLineBackward'}) + .trigger('input', {inputType: 'deleteSoftLineBackward'}) .should('have.value', 'Taiga\n'); }); @@ -58,6 +59,7 @@ describe('Textarea (mask latin letters + digits)', () => { .type('UI and Maskito') .type('{moveToStart}') .trigger('beforeinput', {inputType: 'deleteSoftLineForward'}) + .trigger('input', {inputType: 'deleteSoftLineForward'}) .should('have.value', 'UI and Maskito'); }); });