From ccd344ecd2e6abcff1d7b9f5e7d7fe85a4c20fdd Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Wed, 1 May 2024 14:23:26 -0400 Subject: [PATCH] fix(editor): input editor should call save on focusout or blur of input (#1497) --- .../src/editors/__tests__/floatEditor.spec.ts | 2 +- .../src/editors/__tests__/inputEditor.spec.ts | 18 ++++ .../editors/__tests__/integerEditor.spec.ts | 2 +- packages/common/src/editors/floatEditor.ts | 83 ------------------- packages/common/src/editors/inputEditor.ts | 56 +++++++++++-- packages/common/src/editors/integerEditor.ts | 45 ---------- 6 files changed, 70 insertions(+), 136 deletions(-) diff --git a/packages/common/src/editors/__tests__/floatEditor.spec.ts b/packages/common/src/editors/__tests__/floatEditor.spec.ts index a0b655538..24936def2 100644 --- a/packages/common/src/editors/__tests__/floatEditor.spec.ts +++ b/packages/common/src/editors/__tests__/floatEditor.spec.ts @@ -101,7 +101,7 @@ describe('FloatEditor', () => { editor = new FloatEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-text.editor-price') as HTMLInputElement; - expect(editorElm.ariaLabel).toBe('Price Number Editor'); + expect(editorElm.ariaLabel).toBe('Price Input Editor'); }); it('should initialize the editor and focus on the element after a small delay', () => { diff --git a/packages/common/src/editors/__tests__/inputEditor.spec.ts b/packages/common/src/editors/__tests__/inputEditor.spec.ts index 1b592357f..52bc4d6df 100644 --- a/packages/common/src/editors/__tests__/inputEditor.spec.ts +++ b/packages/common/src/editors/__tests__/inputEditor.spec.ts @@ -390,6 +390,24 @@ describe('InputEditor (TextEditor)', () => { expect(spyCommit).toHaveBeenCalled(); expect(spySave).toHaveBeenCalled(); }); + + it('should call "getEditorLock" and "save" methods when "hasAutoCommitEdit" is enabled and the event "blur" is triggered', () => { + mockItemData = { id: 1, title: 'task', isActive: true }; + gridOptionMock.autoCommitEdit = true; + const spyCommit = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit'); + + editor = new InputEditor(editorArguments, 'text'); + editor.loadValue(mockItemData); + editor.setValue('task 21'); + const spySave = jest.spyOn(editor, 'save'); + const editorElm = editor.editorDomElement; + + editorElm.dispatchEvent(new (window.window as any).Event('blur')); + jest.runAllTimers(); // fast-forward timer + + expect(spyCommit).toHaveBeenCalled(); + expect(spySave).toHaveBeenCalled(); + }); }); describe('validate method', () => { diff --git a/packages/common/src/editors/__tests__/integerEditor.spec.ts b/packages/common/src/editors/__tests__/integerEditor.spec.ts index 3a98aa326..2d81e32cc 100644 --- a/packages/common/src/editors/__tests__/integerEditor.spec.ts +++ b/packages/common/src/editors/__tests__/integerEditor.spec.ts @@ -101,7 +101,7 @@ describe('IntegerEditor', () => { editor = new IntegerEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-text.editor-price') as HTMLInputElement; - expect(editorElm.ariaLabel).toBe('Price Slider Editor'); + expect(editorElm.ariaLabel).toBe('Price Input Editor'); }); it('should initialize the editor and focus on the element after a small delay', () => { diff --git a/packages/common/src/editors/floatEditor.ts b/packages/common/src/editors/floatEditor.ts index 8dde76ff7..55665b7aa 100644 --- a/packages/common/src/editors/floatEditor.ts +++ b/packages/common/src/editors/floatEditor.ts @@ -1,83 +1,13 @@ -import { createDomElement, toSentenceCase } from '@slickgrid-universal/utils'; - import type { EditorArguments, EditorValidationResult } from '../interfaces/index'; import { floatValidator } from '../editorValidators/floatValidator'; import { InputEditor } from './inputEditor'; import { getDescendantProperty } from '../services/utilities'; -const DEFAULT_DECIMAL_PLACES = 0; - export class FloatEditor extends InputEditor { constructor(protected readonly args: EditorArguments) { super(args, 'number'); } - /** Initialize the Editor */ - init() { - if (this.columnDef && this.columnEditor && this.args) { - const columnId = this.columnDef?.id ?? ''; - const compositeEditorOptions = this.args.compositeEditorOptions; - - this._input = createDomElement('input', { - type: 'number', autocomplete: 'off', ariaAutoComplete: 'none', - ariaLabel: this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Number Editor`, - className: `editor-text editor-${columnId}`, - placeholder: this.columnEditor?.placeholder ?? '', - title: this.columnEditor?.title ?? '', - step: `${(this.columnEditor.valueStep !== undefined) ? this.columnEditor.valueStep : this.getInputDecimalSteps()}`, - }); - const cellContainer = this.args.container; - if (cellContainer && typeof cellContainer.appendChild === 'function') { - cellContainer.appendChild(this._input); - } - - this._bindEventService.bind(this._input, 'focus', () => this._input?.select()); - this._bindEventService.bind(this._input, 'keydown', ((event: KeyboardEvent) => { - this._lastInputKeyEvent = event; - if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { - event.stopImmediatePropagation(); - } - }) as EventListener); - - // the lib does not get the focus out event for some reason - // so register it here - if (this.hasAutoCommitEdit && !compositeEditorOptions) { - this._bindEventService.bind(this._input, 'focusout', () => { - this._isValueTouched = true; - this.save(); - }); - } - - if (compositeEditorOptions) { - this._bindEventService.bind(this._input, ['input', 'paste'], this.handleOnInputChange.bind(this) as EventListener); - this._bindEventService.bind(this._input, 'wheel', this.handleOnMouseWheel.bind(this) as EventListener, { passive: true }); - } - } - } - - getDecimalPlaces(): number { - // returns the number of fixed decimal places or null - let rtn = this.columnEditor?.decimal ?? this.columnEditor?.params?.decimalPlaces ?? undefined; - - if (rtn === undefined) { - rtn = DEFAULT_DECIMAL_PLACES; - } - return (!rtn && rtn !== 0 ? null : rtn); - } - - getInputDecimalSteps(): string { - const decimals = this.getDecimalPlaces(); - let zeroString = ''; - for (let i = 1; i < decimals; i++) { - zeroString += '0'; - } - - if (decimals > 0) { - return `0.${zeroString}1`; - } - return '1'; - } - loadValue(item: any) { const fieldName = this.columnDef && this.columnDef.field; @@ -137,17 +67,4 @@ export class FloatEditor extends InputEditor { validator: this.validator, }); } - - // -- - // protected functions - // ------------------ - - /** When the input value changes (this will cover the input spinner arrows on the right) */ - protected handleOnMouseWheel(event: KeyboardEvent) { - this._isValueTouched = true; - const compositeEditorOptions = this.args.compositeEditorOptions; - if (compositeEditorOptions) { - this.handleChangeOnCompositeEditor(event, compositeEditorOptions); - } - } } \ No newline at end of file diff --git a/packages/common/src/editors/inputEditor.ts b/packages/common/src/editors/inputEditor.ts index dec519f7d..55c42969d 100644 --- a/packages/common/src/editors/inputEditor.ts +++ b/packages/common/src/editors/inputEditor.ts @@ -15,6 +15,8 @@ import { getDescendantProperty } from '../services/utilities'; import { textValidator } from '../editorValidators/textValidator'; import { SlickEventData, type SlickGrid } from '../core/index'; +const DEFAULT_DECIMAL_PLACES = 0; + /* * An example of a 'detached' editor. * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. @@ -92,13 +94,18 @@ export class InputEditor implements Editor { const compositeEditorOptions = this.args.compositeEditorOptions; this._input = createDomElement('input', { - type: this._inputType || 'text', - autocomplete: 'off', ariaAutoComplete: 'none', + type: this._inputType || 'text', autocomplete: 'off', ariaAutoComplete: 'none', ariaLabel: this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Input Editor`, + className: `editor-text editor-${columnId}`, placeholder: this.columnEditor?.placeholder ?? '', title: this.columnEditor?.title ?? '', - className: `editor-text editor-${columnId}`, }); + + // add "step" attribute when editor type is integer/float + if (this.inputType === 'number') { + this._input.step = `${(this.columnEditor.valueStep !== undefined) ? this.columnEditor.valueStep : this.getInputDecimalSteps()}`; + } + const cellContainer = this.args.container; if (cellContainer && typeof cellContainer.appendChild === 'function') { cellContainer.appendChild(this._input); @@ -113,10 +120,9 @@ export class InputEditor implements Editor { } }) as EventListener); - // the lib does not get the focus out event for some reason - // so register it here + // listen to focusout or blur to automatically call a save if (this.hasAutoCommitEdit && !compositeEditorOptions) { - this._bindEventService.bind(this._input, 'focusout', () => { + this._bindEventService.bind(this._input, ['focusout', 'blur'], () => { this._isValueTouched = true; this.save(); }); @@ -124,6 +130,11 @@ export class InputEditor implements Editor { if (compositeEditorOptions) { this._bindEventService.bind(this._input, ['input', 'paste'], this.handleOnInputChange.bind(this) as EventListener); + + // add an extra mousewheel listener when editor type is integer/float + if (this.inputType === 'number') { + this._bindEventService.bind(this._input, 'wheel', this.handleOnMouseWheel.bind(this) as EventListener, { passive: true }); + } } } @@ -157,6 +168,30 @@ export class InputEditor implements Editor { this._input?.focus(); } + getDecimalPlaces(): number { + // returns the number of fixed decimal places or null + let rtn = this.columnEditor?.decimal ?? this.columnEditor?.params?.decimalPlaces ?? undefined; + + if (rtn === undefined) { + rtn = DEFAULT_DECIMAL_PLACES; + } + return (!rtn && rtn !== 0 ? null : rtn); + } + + /** when editor is a float input editor, we'll want to know how many decimals to show */ + getInputDecimalSteps(): string { + const decimals = this.getDecimalPlaces(); + let zeroString = ''; + for (let i = 1; i < decimals; i++) { + zeroString += '0'; + } + + if (decimals > 0) { + return `0.${zeroString}1`; + } + return '1'; + } + show() { const isCompositeEditor = !!this.args?.compositeEditorOptions; if (isCompositeEditor) { @@ -338,4 +373,13 @@ export class InputEditor implements Editor { this._timer = setTimeout(() => this.handleChangeOnCompositeEditor(event, compositeEditorOptions), typingDelay); } } + + /** When the input value changes (this will cover the input spinner arrows on the right) */ + protected handleOnMouseWheel(event: KeyboardEvent) { + this._isValueTouched = true; + const compositeEditorOptions = this.args.compositeEditorOptions; + if (compositeEditorOptions) { + this.handleChangeOnCompositeEditor(event, compositeEditorOptions); + } + } } diff --git a/packages/common/src/editors/integerEditor.ts b/packages/common/src/editors/integerEditor.ts index 1ae1ccdec..d6752d60c 100644 --- a/packages/common/src/editors/integerEditor.ts +++ b/packages/common/src/editors/integerEditor.ts @@ -1,5 +1,3 @@ -import { createDomElement, toSentenceCase } from '@slickgrid-universal/utils'; - import type { EditorArguments, EditorValidationResult } from '../interfaces/index'; import { integerValidator } from '../editorValidators/integerValidator'; import { InputEditor } from './inputEditor'; @@ -10,49 +8,6 @@ export class IntegerEditor extends InputEditor { super(args, 'number'); } - /** Initialize the Editor */ - init() { - if (this.columnDef && this.columnEditor && this.args) { - const columnId = this.columnDef?.id ?? ''; - const compositeEditorOptions = this.args.compositeEditorOptions; - - this._input = createDomElement('input', { - type: 'number', autocomplete: 'off', ariaAutoComplete: 'none', - ariaLabel: this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Slider Editor`, - placeholder: this.columnEditor?.placeholder ?? '', - title: this.columnEditor?.title ?? '', - step: `${(this.columnEditor.valueStep !== undefined) ? this.columnEditor.valueStep : '1'}`, - className: `editor-text editor-${columnId}`, - }); - const cellContainer = this.args.container; - if (cellContainer && typeof cellContainer.appendChild === 'function') { - cellContainer.appendChild(this._input); - } - - this._bindEventService.bind(this._input, 'focus', () => this._input?.select()); - this._bindEventService.bind(this._input, 'keydown', ((event: KeyboardEvent) => { - this._lastInputKeyEvent = event; - if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { - event.stopImmediatePropagation(); - } - }) as EventListener); - - // the lib does not get the focus out event for some reason - // so register it here - if (this.hasAutoCommitEdit && !compositeEditorOptions) { - this._bindEventService.bind(this._input, 'focusout', () => { - this._isValueTouched = true; - this.save(); - }); - } - - if (compositeEditorOptions) { - this._bindEventService.bind(this._input, ['input', 'paste'], this.handleOnInputChange.bind(this) as EventListener); - this._bindEventService.bind(this._input, 'wheel', this.handleOnMouseWheel.bind(this) as EventListener, { passive: true }); - } - } - } - loadValue(item: any) { const fieldName = this.columnDef && this.columnDef.field;