Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions packages/ag-grid-community/src/edit/editService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,20 +898,34 @@ export class EditService extends BeanStub implements NamedBean, IEditService {

public setDataValue(position: Required<EditPosition>, newValue: any, eventSource?: string): boolean | undefined {
try {
if ((!this.isEditing() || this.committing) && !SET_DATA_SOURCE_AS_API.has(eventSource)) {
return;
const batch = this.batch;
const editing = this.isEditing(batch ? undefined : position);

if ((!editing || this.committing) && !SET_DATA_SOURCE_AS_API.has(eventSource)) {
return; // Ignore non-edit edits that are not treated as API sources.
}

if (!editing && !batch && eventSource === 'paste') {
return; // Paste on non editable cells and not batching
}

const { beans } = this;
const beans = this.beans;

this.strategy ??= this.createStrategy();
const source = this.batch ? 'ui' : this.committing ? eventSource ?? 'api' : 'api';
let source: string;
if (batch) {
source = 'ui';
} else if (this.committing) {
source = eventSource ?? 'api';
} else {
source = 'api';
}

if (!eventSource || KEEP_EDITOR_SOURCES.has(eventSource)) {
// editApi or undoRedoApi apply change without involving the editor
_syncFromEditor(beans, position, newValue, eventSource, undefined, { persist: true });

if (this.batch) {
if (batch) {
this.cleanupEditors();

_purgeUnchangedEdits(beans);
Expand Down
20 changes: 15 additions & 5 deletions packages/ag-grid-community/src/gridBodyComp/gridBodyCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,22 @@ export class GridBodyCtrl extends BeanStub {
}

public isVerticalScrollShowing(): boolean {
const show = this.gos.get('alwaysShowVerticalScroll');
const { gos, comp, ctrlsSvc } = this;
const show = gos.get('alwaysShowVerticalScroll');

const cssClass = show ? CSS_CLASS_FORCE_VERTICAL_SCROLL : null;
const allowVerticalScroll = _isDomLayout(this.gos, 'normal');
this.comp.setAlwaysVerticalScrollClass(cssClass, show);
const horizontalScrollElement = this.ctrlsSvc.get('center')?.eViewport;
return show || (allowVerticalScroll && _shouldShowVerticalScroll(this.eBodyViewport, horizontalScrollElement));
const allowVerticalScroll = _isDomLayout(gos, 'normal');

comp.setAlwaysVerticalScrollClass(cssClass, show);
const horizontalScrollElement = ctrlsSvc.get('center')?.eViewport;
const hScrollEl = ctrlsSvc.get('fakeHScrollComp')?.getGui();
const vScrollEl = ctrlsSvc.get('fakeVScrollComp')?.getGui();

return (
show ||
(allowVerticalScroll &&
_shouldShowVerticalScroll(this.eBodyViewport, horizontalScrollElement, undefined, vScrollEl, hScrollEl))
);
}

private setupRowAnimationCssClass(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,17 @@ export class RowContainerCtrl extends BeanStub implements ScrollPartner {
}

public isHorizontalScrollShowing(): boolean {
const isAlwaysShowHorizontalScroll = this.gos.get('alwaysShowHorizontalScroll');
const verticalScrollElement = this.beans.ctrlsSvc.getGridBodyCtrl()?.eBodyViewport;
return isAlwaysShowHorizontalScroll || _shouldShowHorizontalScroll(this.eViewport, verticalScrollElement);
const { beans, gos, eViewport } = this;
const isAlwaysShowHorizontalScroll = gos.get('alwaysShowHorizontalScroll');
const { ctrlsSvc } = beans;
const verticalScrollElement = ctrlsSvc.getGridBodyCtrl()?.eBodyViewport;
const hScrollEl = ctrlsSvc.get('fakeHScrollComp')?.getGui();
const vScrollEl = ctrlsSvc.get('fakeVScrollComp')?.getGui();

return (
isAlwaysShowHorizontalScroll ||
_shouldShowHorizontalScroll(eViewport, verticalScrollElement, undefined, hScrollEl, vScrollEl)
);
}

public setHorizontalScroll(offset: number): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@ describe('scrollbarVisibilityHelper', () => {
setSizes({ scrollHeight: 600, scrollWidth: 515 });
expect(_shouldShowHorizontalScroll(element, element, scrollbarWidth)).toBe(true);
});

test('scrollbar suppression respects scrollbar element visibility', () => {
const { element: horizontal } = createElementWithSizes({
clientWidth: 500,
scrollWidth: 508,
});
const { element: vertical } = createElementWithSizes({
clientHeight: 500,
scrollHeight: 508,
});
const { element: hScrollbar, setVisible: setHVisible } = createVisibilityElement(false);
const { element: vScrollbar, setVisible: setVVisible } = createVisibilityElement(true);

expect(_shouldShowHorizontalScroll(horizontal, vertical, scrollbarWidth, hScrollbar, vScrollbar)).toBe(true);

setHVisible(true);
expect(_shouldShowHorizontalScroll(horizontal, vertical, scrollbarWidth, hScrollbar, vScrollbar)).toBe(false);

setVVisible(false);
expect(_shouldShowHorizontalScroll(horizontal, vertical, scrollbarWidth, hScrollbar, vScrollbar)).toBe(true);
});
});

type ElementDimensions = {
Expand Down Expand Up @@ -93,3 +114,18 @@ function createElementWithSizes(initial: ElementDimensions) {
},
};
}

function createVisibilityElement(initiallyVisible: boolean) {
let visible = initiallyVisible;
const element = document.createElement('div') as HTMLElement & {
checkVisibility?: (options?: unknown) => boolean;
};
element.checkVisibility = () => visible;

return {
element,
setVisible(nextVisible: boolean) {
visible = nextVisible;
},
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { _getScrollbarWidth } from '../agStack/utils/browser';
import { _isVisible } from '../agStack/utils/dom';

const AXES = {
horizontal: {
Expand All @@ -18,27 +19,49 @@ const AXES = {
export function _shouldShowHorizontalScroll(
horizontalElement: HTMLElement,
verticalScrollElement?: HTMLElement,
scrollbarWidth: number = _getScrollbarWidth() || 0
scrollbarWidth: number = _getScrollbarWidth() || 0,
primaryScrollbarElement?: HTMLElement,
oppositeScrollbarElement?: HTMLElement
): boolean {
return shouldShowScroll(horizontalElement, verticalScrollElement, 'horizontal', scrollbarWidth);
return shouldShowScroll(
horizontalElement,
verticalScrollElement,
'horizontal',
scrollbarWidth,
primaryScrollbarElement,
oppositeScrollbarElement
);
}

export function _shouldShowVerticalScroll(
verticalElement: HTMLElement,
horizontalScrollElement?: HTMLElement,
scrollbarWidth: number = _getScrollbarWidth() || 0
scrollbarWidth: number = _getScrollbarWidth() || 0,
primaryScrollbarElement?: HTMLElement,
oppositeScrollbarElement?: HTMLElement
): boolean {
return shouldShowScroll(verticalElement, horizontalScrollElement, 'vertical', scrollbarWidth);
return shouldShowScroll(
verticalElement,
horizontalScrollElement,
'vertical',
scrollbarWidth,
primaryScrollbarElement,
oppositeScrollbarElement
);
}

function shouldShowScroll(
primaryElement: HTMLElement,
oppositeElement: HTMLElement | undefined,
axis: 'horizontal' | 'vertical',
scrollbarWidth: number
scrollbarWidth: number,
primaryScrollbarElement: HTMLElement | undefined,
oppositeScrollbarElement: HTMLElement | undefined
): boolean {
const primary = AXES[axis];
const opposite = AXES[primary.opposite];
const primaryScrollbarShowing = primaryScrollbarElement ? _isVisible(primaryScrollbarElement) : true;
const oppositeScrollbarShowing = oppositeScrollbarElement ? _isVisible(oppositeScrollbarElement) : true;

const primaryOverflow = primary.overflow(primaryElement);
if (primaryOverflow <= 0) {
Expand All @@ -55,14 +78,16 @@ function shouldShowScroll(
}

if (primaryOverflow <= scrollbarWidth) {
const oppositeCausedByPrimary = isScrollbarCausedByOppositeAxis({
candidateOverflow: oppositeOverflow,
candidateScrollSize: opposite.scrollSize(oppositeElement),
candidateClientSize: opposite.clientSize(oppositeElement),
scrollbarWidth,
});

if (oppositeCausedByPrimary) {
if (
primaryScrollbarShowing &&
oppositeScrollbarShowing &&
isScrollbarCausedByOppositeAxis({
candidateOverflow: oppositeOverflow,
candidateScrollSize: opposite.scrollSize(oppositeElement),
candidateClientSize: opposite.clientSize(oppositeElement),
scrollbarWidth,
})
) {
// The opposite scrollbar only exists because of this one, so suppress this scrollbar.
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,39 +90,48 @@ export class FormulaInputAutocompleteFeature extends BeanStub {
return;
}

const value = this.field.getCurrentValue();
const { field, beans } = this;

const value = field.getCurrentValue();
const hasFormulaPrefix = value.trimStart().startsWith('=');

if (!hasFormulaPrefix) {
this.closeFunctionAutocomplete();
return;
}

const caretOffsets = this.field.getCaretOffsetsForAutocomplete(value);
const caretOffsets = field.getCaretOffsetsForAutocomplete(value);
if (!caretOffsets) {
this.closeFunctionAutocomplete();
return;
}

if (isCaretInsideRefToken(this.beans, value, caretOffsets.valueOffset)) {
if (isCaretInsideRefToken(beans, value, caretOffsets.valueOffset)) {
this.closeFunctionAutocomplete();
return;
}

const token = getFunctionTokenAtOffset(value, caretOffsets.valueOffset, this.beans.formula ?? null);
const token = getFunctionTokenAtOffset(value, caretOffsets.valueOffset, beans.formula ?? null);
if (!token) {
this.closeFunctionAutocomplete();
return;
}

const { prefix } = token;

if (!prefix.length) {
this.closeFunctionAutocomplete();
return;
}

const entries = this.getFunctionAutocompleteEntries();
if (!entries.length) {
this.closeFunctionAutocomplete();
return;
}

const searchLower = token.prefix.toLocaleLowerCase();
const hasMatch = entries.some((entry) => entry.key.toLocaleLowerCase().startsWith(searchLower));
const searchLower = prefix.toLocaleLowerCase();
const hasMatch = entries.some(({ key }) => key.toLocaleLowerCase().startsWith(searchLower));

if (!hasMatch) {
this.closeFunctionAutocomplete();
Expand All @@ -132,9 +141,9 @@ export class FormulaInputAutocompleteFeature extends BeanStub {
this.functionAutocompleteToken = token;
this.openFunctionAutocomplete(entries);

if (this.functionAutocompleteList && this.functionAutocompleteSearch !== token.prefix) {
this.functionAutocompleteList.setSearch(token.prefix);
this.functionAutocompleteSearch = token.prefix;
if (this.functionAutocompleteList && this.functionAutocompleteSearch !== prefix) {
this.functionAutocompleteList.setSearch(prefix);
this.functionAutocompleteSearch = prefix;
}
}

Expand Down Expand Up @@ -222,15 +231,16 @@ export class FormulaInputAutocompleteFeature extends BeanStub {
return;
}

const value = this.field.getCurrentValue();
const { field } = this;
const value = field.getCurrentValue();
const functionName = selected.key;
const baseValue = value.slice(0, token.start) + functionName + value.slice(token.end);
const insertPos = token.start + functionName.length;
const nextValue =
baseValue[insertPos] === '(' ? baseValue : baseValue.slice(0, insertPos) + '(' + baseValue.slice(insertPos);

this.field.getContentElement().focus({ preventScroll: true });
this.field.applyFormulaValueChange({
field.getContentElement().focus({ preventScroll: true });
field.applyFormulaValueChange({
currentValue: value,
nextValue,
caret: insertPos + 1,
Expand Down
68 changes: 64 additions & 4 deletions testing/behavioural/src/cell-editing/cell-editing-batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { userEvent } from '@testing-library/user-event';
import { agTestIdFor, getGridElement, setupAgTestIds } from 'ag-grid-community';
import { BatchEditModule } from 'ag-grid-enterprise';

import { TestGridsManager, asyncSetTimeout, waitForInput } from '../test-utils';
import { GridRows, TestGridsManager, asyncSetTimeout, waitForInput } from '../test-utils';
import { expect } from '../test-utils/matchers';

describe('Cell Editing Batch', () => {
Expand Down Expand Up @@ -216,10 +216,14 @@ describe('Cell Editing Batch', () => {
const cellB = getByTestId(gridDiv, agTestIdFor.cell('0', 'b'));
expect(cellB).toHaveTextContent('initial');

await userEvent.dblClick(cellA);
const editor = await waitForInput(gridDiv, cellA, { popup: false });
api.startEditingCell({ rowIndex: 0, colKey: 'a' });
await asyncSetTimeout(1);
const editor = gridDiv.querySelector<HTMLInputElement>('input');
if (!editor) {
throw new Error('Editor input not found');
}
await userEvent.clear(editor);
await userEvent.type(editor, 'xx{Enter}');
await userEvent.keyboard('xx{Enter}');
await asyncSetTimeout(1);

api.refreshCells({ columns: ['b'], force: true });
Expand Down Expand Up @@ -250,4 +254,60 @@ describe('Cell Editing Batch', () => {

expect(cellB).toHaveTextContent('xx');
});

test('setDataValue during batch edit is staged for new cells', async () => {
const api = await gridMgr.createGridAndWait('myGrid', {
columnDefs: [
{ field: 'number', editable: true, cellEditor: 'agNumberCellEditor' },
{ field: 'string1', editable: true, cellEditor: 'agTextCellEditor' },
],
rowData: [{ number: 10, string1: 'test' }],
});

api.startBatchEdit();

const beforeRows = new GridRows(api, 'before batch setDataValue');
await beforeRows.check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:0 number:10 string1:"test"
`);

const gridDiv = getGridElement(api)! as HTMLElement;
await asyncSetTimeout(1);
const numberCell = getByTestId(gridDiv, agTestIdFor.cell('0', 'number'));
const stringCell = getByTestId(gridDiv, agTestIdFor.cell('0', 'string1'));

await userEvent.dblClick(numberCell);
await asyncSetTimeout(1);
await userEvent.keyboard('100{Enter}');
await asyncSetTimeout(1);

expect(numberCell).toHaveTextContent('100');
expect(numberCell).toHaveClass(/ag-cell-batch-edit/);

const rowNode = api.getDisplayedRowAtIndex(0);
rowNode?.setDataValue('string1', 'pending', 'ui');
await asyncSetTimeout(1);

await new GridRows(api, 'after batch setDataValue ui').check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:0 number:100 string1:"pending"
`);

expect(stringCell).toHaveTextContent('pending');

api.cancelBatchEdit();
await asyncSetTimeout(1);

await new GridRows(api, 'after cancel batch setDataValue').check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:0 number:10 string1:"test"
`);

expect(numberCell).toHaveTextContent('10');
expect(stringCell).toHaveTextContent('test');
expect(numberCell).not.toHaveClass(/ag-cell-batch-edit/);
expect(stringCell).not.toHaveClass(/ag-cell-batch-edit/);
expect(api.getDisplayedRowAtIndex(0)?.data?.string1).toBe('test');
});
});
Loading
Loading