From de7af783fef147e394cdbd8e537eb99954a42abb Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Mon, 30 Mar 2026 14:11:28 +0200 Subject: [PATCH 1/4] feat: useColumnWidth for inline longText editor --- demos/vanilla/src/examples/example12.ts | 2 +- .../editors/__tests__/longTextEditor.spec.ts | 22 ++++++++ packages/common/src/editors/longTextEditor.ts | 19 ++++++- .../longTextEditorOption.interface.ts | 7 +++ test/cypress/e2e/example12.cy.ts | 53 +++++++++++++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/demos/vanilla/src/examples/example12.ts b/demos/vanilla/src/examples/example12.ts index bf6b14f0f1..aca2fd9722 100644 --- a/demos/vanilla/src/examples/example12.ts +++ b/demos/vanilla/src/examples/example12.ts @@ -173,7 +173,7 @@ export default class Example12 { alwaysSaveOnEnterKey: true, maxLength: 12, options: { - cols: 45, + useColumnWidth: true, rows: 6, buttonTexts: { cancel: 'Close', diff --git a/packages/common/src/editors/__tests__/longTextEditor.spec.ts b/packages/common/src/editors/__tests__/longTextEditor.spec.ts index e46651ba1e..2864b4cc53 100644 --- a/packages/common/src/editors/__tests__/longTextEditor.spec.ts +++ b/packages/common/src/editors/__tests__/longTextEditor.spec.ts @@ -43,6 +43,7 @@ const gridStub = { navigatePrev: vi.fn(), render: vi.fn(), onBeforeEditCell: new SlickEvent(), + onColumnsResized: new SlickEvent(), onCompositeEditorChange: new SlickEvent(), } as unknown as SlickGrid; @@ -174,6 +175,27 @@ describe('LongTextEditor', () => { expect(editor.editorDomElement.rows).toBe(6); }); + it('should initialize the editor using the container (column) width when useColumnWidth is enabled', () => { + Object.defineProperty(editorArguments.container, 'offsetWidth', { configurable: true, value: 180 }); + mockColumn.editor!.options = { useColumnWidth: true } as LongTextEditorOption; + editor = new LongTextEditor(editorArguments); + + expect(editor.editorDomElement.style.width).toBe('180px'); + }); + + it('should update the textarea width when a column is resized and useColumnWidth is enabled', () => { + Object.defineProperty(editorArguments.container, 'offsetWidth', { configurable: true, value: 180 }); + mockColumn.editor!.options = { useColumnWidth: true } as LongTextEditorOption; + editor = new LongTextEditor(editorArguments); + + expect(editor.editorDomElement.style.width).toBe('180px'); + + Object.defineProperty(editorArguments.container, 'offsetWidth', { configurable: true, value: 240 }); + gridStub.onColumnsResized.notify({ triggeredByColumn: 'title', grid: gridStub }); + + expect(editor.editorDomElement.style.width).toBe('240px'); + }); + it('should have a placeholder when defined in its column definition', () => { const testValue = 'test placeholder'; mockColumn.editor!.placeholder = testValue; diff --git a/packages/common/src/editors/longTextEditor.ts b/packages/common/src/editors/longTextEditor.ts index 3e9340611c..99ebe74b80 100644 --- a/packages/common/src/editors/longTextEditor.ts +++ b/packages/common/src/editors/longTextEditor.ts @@ -1,6 +1,6 @@ import { BindingEventService } from '@slickgrid-universal/binding'; import { createDomElement, getOffset, setDeepValue, toSentenceCase, type HtmlElementPosition } from '@slickgrid-universal/utils'; -import { SlickEventData, type SlickGrid } from '../core/index.js'; +import { SlickEventData, SlickEventHandler, type SlickGrid } from '../core/index.js'; import { textValidator } from '../editorValidators/textValidator.js'; import type { Column, @@ -27,6 +27,7 @@ import { Constants } from './../constants.js'; */ export class LongTextEditor implements Editor { protected _bindEventService: BindingEventService; + protected _eventHandler: SlickEventHandler; protected _defaultTextValue: any; protected _isValueTouched = false; protected _locales: Locale; @@ -59,6 +60,7 @@ export class LongTextEditor implements Editor { this._locales = this.gridOptions?.locales || Constants.locales; this._bindEventService = new BindingEventService(); + this._eventHandler = new SlickEventHandler(); this.init(); } @@ -126,6 +128,7 @@ export class LongTextEditor implements Editor { containerElm.appendChild(this._wrapperElm); // use textarea row if defined but don't go over 3 rows with composite editor modal + const useColWidth = !this.args.isCompositeEditor && (this.editorOptions?.useColumnWidth ?? false); this._textareaElm = createDomElement( 'textarea', { @@ -134,10 +137,15 @@ export class LongTextEditor implements Editor { rows: this.args.isCompositeEditor && textAreaRows > 3 ? 3 : textAreaRows, placeholder: this.columnEditor?.placeholder ?? '', title: this.columnEditor?.title ?? '', + style: useColWidth ? { width: `${this.args.container.offsetWidth}px` } : {}, }, this._wrapperElm ); + if (useColWidth) { + this._eventHandler.subscribe(this.grid.onColumnsResized, this.handleColumnsResized.bind(this)); + } + const editorFooterElm = createDomElement('div', { className: 'editor-footer' }); const countContainerElm = createDomElement('span', { className: 'counter' }); this._currentLengthElm = createDomElement('span', { className: 'text-length', textContent: '0' }); @@ -200,6 +208,7 @@ export class LongTextEditor implements Editor { destroy(): void { clearTimeout(this._timer); + this._eventHandler.unsubscribeAll(); this._bindEventService.unbindAll(); this._wrapperElm?.remove(); } @@ -421,6 +430,12 @@ export class LongTextEditor implements Editor { this.disable(isCellEditable === false); } + protected handleColumnsResized(): void { + if (this._textareaElm) { + this._textareaElm.style.width = `${this.args.container.offsetWidth}px`; + } + } + protected handleKeyDown(e: KeyboardEvent): void { const key = e.key; this._isValueTouched = true; @@ -440,7 +455,7 @@ export class LongTextEditor implements Editor { } /** On every input change event, we'll update the current text length counter */ - protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement }): void { + protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement; }): void { const compositeEditorOptions = this.args.compositeEditorOptions; const maxLength = this.columnEditor?.maxLength; diff --git a/packages/common/src/interfaces/longTextEditorOption.interface.ts b/packages/common/src/interfaces/longTextEditorOption.interface.ts index c9fc82334a..76b5879374 100644 --- a/packages/common/src/interfaces/longTextEditorOption.interface.ts +++ b/packages/common/src/interfaces/longTextEditorOption.interface.ts @@ -5,6 +5,13 @@ export interface LongTextEditorOption { */ cols?: number; + /** + * Defaults to false, when enabled the editor textarea will use the column width (in pixels) instead of the `cols` setting, + * and will also auto-resize the textarea width whenever the column itself gets resized. + * Note: this only applies to Inline Editing and will not have any effect when using the Composite Editor modal window. + */ + useColumnWidth?: boolean; + /** * Defaults to 6, that is the number of visible text lines for the textarea control. * Note: this only applies to Inline Editing and will not have any effect when using the Composite Editor modal window which will be fixed to 3 rows. diff --git a/test/cypress/e2e/example12.cy.ts b/test/cypress/e2e/example12.cy.ts index e9746743b4..8b63ba34df 100644 --- a/test/cypress/e2e/example12.cy.ts +++ b/test/cypress/e2e/example12.cy.ts @@ -894,4 +894,57 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.vc:visible .vc-year').should('have.attr', 'data-vc-year').should('contain', today.getFullYear()); cy.get('.slick-editor-modal-footer .btn-cancel').click(); }); + + it('should open the "Title" LongText editor and expect its textarea width to match the column width', () => { + cy.get('.grid12') + .find('.slick-header-columns:nth(1)') + .children('.slick-header-column:nth(1)') + .should('contain', 'Title') + .then(($col) => { + const columnWidth = $col[0].getBoundingClientRect().width; + + cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(1)`).click(); + + cy.get('.slick-large-editor-text.editor-title textarea').then(($textarea) => { + const textareaWidth = parseFloat($textarea[0].style.width); + // account for borders/padding + expect(textareaWidth).to.be.closeTo(columnWidth, 5); + }); + + cy.get('.editor-title .editor-footer .btn-cancel').click(); + }); + }); + + it('should resize the "Title" column and expect the textarea width to update accordingly when the editor is reopened', () => { + cy.get('.grid12') + .find('.slick-header-columns:nth(1)') + .children('.slick-header-column:nth(1)') + .should('contain', 'Title') + .then(($col) => { + const widthBefore = $col[0].getBoundingClientRect().width; + + cy.get('.grid12 .slick-resizable-handle:nth(0)') + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', { clientX: $col[0].getBoundingClientRect().right + 80, force: true }); + cy.get('.grid12 .slick-header-column:nth(2)') + .trigger('mousemove', 'right') + .trigger('mouseup', { which: 1, force: true }); + + cy.get('.grid12') + .find('.slick-header-columns:nth(1)') + .children('.slick-header-column:nth(1)') + .then(($colAfter) => { + const widthAfter = $colAfter[0].getBoundingClientRect().width; + expect(widthAfter).to.be.greaterThan(widthBefore); + + cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(1)`).click(); + cy.get('.slick-large-editor-text.editor-title textarea').then(($textarea) => { + const textareaWidth = parseFloat($textarea[0].style.width); + expect(textareaWidth).to.be.closeTo(widthAfter, 5); + }); + + cy.get('.editor-title .editor-footer .btn-cancel').click(); + }); + }); + }); }); From c1a3c088e6d76235c5f0e29f0e4cf9224759a729 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Mon, 30 Mar 2026 14:19:34 +0200 Subject: [PATCH 2/4] chore: linter fixes --- packages/common/src/editors/longTextEditor.ts | 2 +- test/cypress/e2e/example12.cy.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/common/src/editors/longTextEditor.ts b/packages/common/src/editors/longTextEditor.ts index 99ebe74b80..041d0d75b1 100644 --- a/packages/common/src/editors/longTextEditor.ts +++ b/packages/common/src/editors/longTextEditor.ts @@ -455,7 +455,7 @@ export class LongTextEditor implements Editor { } /** On every input change event, we'll update the current text length counter */ - protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement; }): void { + protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement }): void { const compositeEditorOptions = this.args.compositeEditorOptions; const maxLength = this.columnEditor?.maxLength; diff --git a/test/cypress/e2e/example12.cy.ts b/test/cypress/e2e/example12.cy.ts index 8b63ba34df..7794fd4624 100644 --- a/test/cypress/e2e/example12.cy.ts +++ b/test/cypress/e2e/example12.cy.ts @@ -926,9 +926,7 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.grid12 .slick-resizable-handle:nth(0)') .trigger('mousedown', { which: 1, force: true }) .trigger('mousemove', { clientX: $col[0].getBoundingClientRect().right + 80, force: true }); - cy.get('.grid12 .slick-header-column:nth(2)') - .trigger('mousemove', 'right') - .trigger('mouseup', { which: 1, force: true }); + cy.get('.grid12 .slick-header-column:nth(2)').trigger('mousemove', 'right').trigger('mouseup', { which: 1, force: true }); cy.get('.grid12') .find('.slick-header-columns:nth(1)') From f75fc613149beb21d0b7dfe1e56cf00aede75d89 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Tue, 31 Mar 2026 08:51:02 +0200 Subject: [PATCH 3/4] feat: adjust spacing to match the cells width --- packages/common/src/editors/longTextEditor.ts | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/common/src/editors/longTextEditor.ts b/packages/common/src/editors/longTextEditor.ts index 041d0d75b1..6b67e56bd3 100644 --- a/packages/common/src/editors/longTextEditor.ts +++ b/packages/common/src/editors/longTextEditor.ts @@ -137,7 +137,7 @@ export class LongTextEditor implements Editor { rows: this.args.isCompositeEditor && textAreaRows > 3 ? 3 : textAreaRows, placeholder: this.columnEditor?.placeholder ?? '', title: this.columnEditor?.title ?? '', - style: useColWidth ? { width: `${this.args.container.offsetWidth}px` } : {}, + style: useColWidth ? { width: `${this._getContainerInnerWidth()}px` } : {}, }, this._wrapperElm ); @@ -329,8 +329,11 @@ export class LongTextEditor implements Editor { const calculatedBodyWidth = document.body.offsetWidth || window.innerWidth; // first defined position will be bottom/right (which will position the editor completely over the cell) + const containerStyle = getComputedStyle(this.args.container); let newPositionTop = this.args.container ? containerOffset.top : (parentPosition.top ?? 0); - let newPositionLeft = this.args.container ? containerOffset.left : (parentPosition.left ?? 0); + let newPositionLeft = this.args.container + ? containerOffset.left + parseFloat(containerStyle.paddingLeft) - 1 + : (parentPosition.left ?? 0); // user could explicitely use a "left" position (when user knows his column is completely on the right) // or when using "auto" and we detect not enough available space then we'll position to the "left" of the cell @@ -432,10 +435,23 @@ export class LongTextEditor implements Editor { protected handleColumnsResized(): void { if (this._textareaElm) { - this._textareaElm.style.width = `${this.args.container.offsetWidth}px`; + this._textareaElm.style.width = `${this._getContainerInnerWidth()}px`; } } + protected _getContainerInnerWidth(): number { + const container = this.args.container; + const containerStyle = getComputedStyle(container); + const wrapperStyle = getComputedStyle(this._wrapperElm); + return ( + container.clientWidth - + parseFloat(containerStyle.paddingLeft) - + parseFloat(containerStyle.paddingRight) - + parseFloat(wrapperStyle.paddingLeft) - + parseFloat(wrapperStyle.paddingRight) + ); + } + protected handleKeyDown(e: KeyboardEvent): void { const key = e.key; this._isValueTouched = true; @@ -455,7 +471,7 @@ export class LongTextEditor implements Editor { } /** On every input change event, we'll update the current text length counter */ - protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement }): void { + protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement; }): void { const compositeEditorOptions = this.args.compositeEditorOptions; const maxLength = this.columnEditor?.maxLength; From 54c1003ad5aff39b81f9075930efc0d47bbed950 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Tue, 31 Mar 2026 12:22:55 +0200 Subject: [PATCH 4/4] chore: fix tests and linter issues --- .../editors/__tests__/longTextEditor.spec.ts | 6 +++--- packages/common/src/editors/longTextEditor.ts | 17 ++++++++--------- test/cypress/e2e/example12.cy.ts | 6 ++++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/common/src/editors/__tests__/longTextEditor.spec.ts b/packages/common/src/editors/__tests__/longTextEditor.spec.ts index 2864b4cc53..b584e294b5 100644 --- a/packages/common/src/editors/__tests__/longTextEditor.spec.ts +++ b/packages/common/src/editors/__tests__/longTextEditor.spec.ts @@ -176,7 +176,7 @@ describe('LongTextEditor', () => { }); it('should initialize the editor using the container (column) width when useColumnWidth is enabled', () => { - Object.defineProperty(editorArguments.container, 'offsetWidth', { configurable: true, value: 180 }); + Object.defineProperty(editorArguments.container, 'clientWidth', { configurable: true, value: 180 }); mockColumn.editor!.options = { useColumnWidth: true } as LongTextEditorOption; editor = new LongTextEditor(editorArguments); @@ -184,13 +184,13 @@ describe('LongTextEditor', () => { }); it('should update the textarea width when a column is resized and useColumnWidth is enabled', () => { - Object.defineProperty(editorArguments.container, 'offsetWidth', { configurable: true, value: 180 }); + Object.defineProperty(editorArguments.container, 'clientWidth', { configurable: true, value: 180 }); mockColumn.editor!.options = { useColumnWidth: true } as LongTextEditorOption; editor = new LongTextEditor(editorArguments); expect(editor.editorDomElement.style.width).toBe('180px'); - Object.defineProperty(editorArguments.container, 'offsetWidth', { configurable: true, value: 240 }); + Object.defineProperty(editorArguments.container, 'clientWidth', { configurable: true, value: 240 }); gridStub.onColumnsResized.notify({ triggeredByColumn: 'title', grid: gridStub }); expect(editor.editorDomElement.style.width).toBe('240px'); diff --git a/packages/common/src/editors/longTextEditor.ts b/packages/common/src/editors/longTextEditor.ts index 6b67e56bd3..c136f5f16e 100644 --- a/packages/common/src/editors/longTextEditor.ts +++ b/packages/common/src/editors/longTextEditor.ts @@ -329,11 +329,10 @@ export class LongTextEditor implements Editor { const calculatedBodyWidth = document.body.offsetWidth || window.innerWidth; // first defined position will be bottom/right (which will position the editor completely over the cell) - const containerStyle = getComputedStyle(this.args.container); + // offset left by the container's padding-left so the wrapper aligns with the cell's text content + const containerPaddingLeft = parseFloat(getComputedStyle(this.args.container).paddingLeft) || 0; let newPositionTop = this.args.container ? containerOffset.top : (parentPosition.top ?? 0); - let newPositionLeft = this.args.container - ? containerOffset.left + parseFloat(containerStyle.paddingLeft) - 1 - : (parentPosition.left ?? 0); + let newPositionLeft = this.args.container ? containerOffset.left + containerPaddingLeft : (parentPosition.left ?? 0); // user could explicitely use a "left" position (when user knows his column is completely on the right) // or when using "auto" and we detect not enough available space then we'll position to the "left" of the cell @@ -445,10 +444,10 @@ export class LongTextEditor implements Editor { const wrapperStyle = getComputedStyle(this._wrapperElm); return ( container.clientWidth - - parseFloat(containerStyle.paddingLeft) - - parseFloat(containerStyle.paddingRight) - - parseFloat(wrapperStyle.paddingLeft) - - parseFloat(wrapperStyle.paddingRight) + (parseFloat(containerStyle.paddingLeft) || 0) - + (parseFloat(containerStyle.paddingRight) || 0) - + (parseFloat(wrapperStyle.paddingLeft) || 0) - + (parseFloat(wrapperStyle.paddingRight) || 0) ); } @@ -471,7 +470,7 @@ export class LongTextEditor implements Editor { } /** On every input change event, we'll update the current text length counter */ - protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement; }): void { + protected handleOnInputChange(event: Event & { clipboardData: DataTransfer; target: HTMLTextAreaElement }): void { const compositeEditorOptions = this.args.compositeEditorOptions; const maxLength = this.columnEditor?.maxLength; diff --git a/test/cypress/e2e/example12.cy.ts b/test/cypress/e2e/example12.cy.ts index 7794fd4624..8d8d5dfdce 100644 --- a/test/cypress/e2e/example12.cy.ts +++ b/test/cypress/e2e/example12.cy.ts @@ -908,7 +908,8 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.slick-large-editor-text.editor-title textarea').then(($textarea) => { const textareaWidth = parseFloat($textarea[0].style.width); // account for borders/padding - expect(textareaWidth).to.be.closeTo(columnWidth, 5); + const paddings = 10; + expect(textareaWidth).to.be.closeTo(columnWidth - paddings, 20); }); cy.get('.editor-title .editor-footer .btn-cancel').click(); @@ -938,7 +939,8 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(1)`).click(); cy.get('.slick-large-editor-text.editor-title textarea').then(($textarea) => { const textareaWidth = parseFloat($textarea[0].style.width); - expect(textareaWidth).to.be.closeTo(widthAfter, 5); + const paddings = 10; + expect(textareaWidth).to.be.closeTo(widthAfter - paddings, 20); }); cy.get('.editor-title .editor-footer .btn-cancel').click();