import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Column, GridOption } from '../models';
import { SlickGrid } from '../slick.grid';
import { SlickEventData } from '../slick.core';
vi.useFakeTimers();
const DEFAULT_GRID_HEIGHT = 600;
const DEFAULT_GRID_WIDTH = 800;
const gridId = 'grid1';
const gridUid = 'slickgrid_124343';
const containerId = 'demo-container';
const template = `
`;
describe('SlickGrid RTL (Right-to-Left)', () => {
let container: HTMLElement;
let grid: SlickGrid;
const items = [
{ id: 0, name: 'Item 0', value: 10 },
{ id: 1, name: 'Item 1', value: 20 },
{ id: 2, name: 'Item 2', value: 30 },
];
const columns = [
{ id: 'id', field: 'id', name: 'ID', width: 60, resizable: true },
{ id: 'name', field: 'name', name: 'Name', width: 100, resizable: true },
{ id: 'value', field: 'value', name: 'Value', width: 80, resizable: true },
] as Column[];
let defaultOptions: GridOption;
beforeEach(() => {
defaultOptions = {
enableCellNavigation: true,
columnResizingDelay: 1,
scrollRenderThrottling: 1,
errorCallback: vi.fn(),
};
container = document.createElement('div');
container.id = gridId;
container.innerHTML = template;
container.style.height = `${DEFAULT_GRID_HEIGHT}px`;
container.style.width = `${DEFAULT_GRID_WIDTH}px`;
document.body.appendChild(container);
Object.defineProperty(container, 'height', { writable: true, configurable: true, value: DEFAULT_GRID_HEIGHT });
Object.defineProperty(container, 'clientHeight', { writable: true, configurable: true, value: DEFAULT_GRID_HEIGHT });
Object.defineProperty(container, 'clientWidth', { writable: true, configurable: true, value: DEFAULT_GRID_WIDTH });
});
afterEach(() => {
document.body.textContent = '';
grid?.destroy(true);
});
describe('RTL Option', () => {
it('should have rtl option set to false by default', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, defaultOptions);
expect(grid.getOptions().rtl).toBe(false);
});
it('should ignore a change from LTR to RTL after grid creation', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, defaultOptions);
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
grid.setOptions({ rtl: true, enableCellNavigation: false });
expect(grid.getOptions().rtl).toBe(false);
expect(grid.getOptions().enableCellNavigation).toBe(false);
expect(gridContainer.classList.contains('slick-rtl')).toBe(false);
expect(gridContainer.getAttribute('dir')).toBeNull();
expect(warnSpy).toHaveBeenCalledWith('[SlickGrid] the "rtl" option is only applied when the grid is created; the change was ignored.');
warnSpy.mockRestore();
});
it('should ignore a change from RTL to LTR after grid creation', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true });
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
grid.setOptions({ rtl: false });
expect(grid.getOptions().rtl).toBe(true);
expect(gridContainer.classList.contains('slick-rtl')).toBe(true);
expect(gridContainer.getAttribute('dir')).toBe('rtl');
expect(warnSpy).toHaveBeenCalledWith('[SlickGrid] the "rtl" option is only applied when the grid is created; the change was ignored.');
warnSpy.mockRestore();
});
it('should enable RTL mode when rtl option is set to true', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true });
expect(grid.getOptions().rtl).toBe(true);
});
it('should apply RTL class and dir attribute on grid container', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true });
expect(gridContainer.classList.contains('slick-rtl')).toBe(true);
expect(gridContainer.getAttribute('dir')).toBe('rtl');
});
it('should not apply RTL class or dir in LTR mode', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: false });
expect(gridContainer.classList.contains('slick-rtl')).toBe(false);
expect(gridContainer.getAttribute('dir')).toBeNull();
});
});
describe('Visible Range in RTL', () => {
it('should calculate leftPx/rightPx with RTL negative scrollLeft convention', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true });
const anyGrid = grid as any;
anyGrid.canvasWidth = 2000;
anyGrid.viewportW = 800;
const range = grid.getVisibleRange(0, -200);
expect(range.leftPx).toBe(200);
expect(range.rightPx).toBe(1000);
expect(range.rightPx).toBeGreaterThan(range.leftPx);
});
});
describe('Column Resizing in RTL', () => {
it('should handle RTL mode with resizable columns', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true, editable: true });
expect(grid.getOptions().rtl).toBe(true);
});
});
describe('applyColumnWidths with RTL', () => {
it('should apply column widths in RTL mode', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true });
expect(grid.getOptions().rtl).toBe(true);
});
it('should apply column widths in LTR mode', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: false });
expect(grid.getOptions().rtl).toBe(false);
});
});
describe('Resize constraints with RTL', () => {
it('should calculate resize constraints correctly in RTL mode', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true, editable: true });
expect(grid.getOptions().rtl).toBe(true);
});
it('should calculate resize constraints correctly in LTR mode', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: false, editable: true });
expect(grid.getOptions().rtl).toBe(false);
});
});
describe('Mixed RTL Features', () => {
it('should support RTL with pinned columns', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true, pinning: { columns: { left: 0 } } });
expect(grid.getOptions().rtl).toBe(true);
expect(grid.getOptions().pinning?.columns?.left).toBe(0);
});
it('should support RTL with sorting', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true });
expect(grid.getOptions().rtl).toBe(true);
});
it('should support RTL with filtering', () => {
const gridContainer = document.getElementById(gridId) as HTMLElement;
grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true });
expect(grid.getOptions().rtl).toBe(true);
});
});
describe('Column Resizing', () => {
const columns = [
{ id: 'id', field: 'id', name: 'Id', hidden: true },
{ id: 'firstName', field: 'firstName', name: 'First Name', sortable: true, width: 77, previousWidth: 20, rerenderOnResize: true },
{ id: 'lastName', field: 'lastName', name: 'Last Name', sortable: true, minWidth: 35, maxWidth: 78 },
{ id: 'age', field: 'age', name: 'Age', sortable: true, minWidth: 82, width: 86, maxWidth: 88 },
{ id: 'gender', field: 'gender', name: 'Gender', sortable: true },
] as Column[];
const data = [
{ id: 0, firstName: 'John', lastName: 'Doe', age: 30 },
{ id: 1, firstName: 'Jane', lastName: 'Doe', age: 28 },
];
it('should resize 2nd column that has a "width" defined using default sizing grid options', () => {
grid = new SlickGrid(container, data, columns, { ...defaultOptions, forceFitColumns: false, rtl: true });
grid.init();
const sedOnBeforeResize = new SlickEventData();
sedOnBeforeResize.addReturnValue(true);
vi.spyOn(grid.onBeforeColumnsResize, 'notify').mockReturnValue(sedOnBeforeResize);
const onColumnsDragSpy = vi.spyOn(grid.onColumnsDrag, 'notify');
const onColumnsResizedSpy = vi.spyOn(grid.onColumnsResized, 'notify');
const columnElms = container.querySelectorAll('.slick-header-column');
const resizeHandleElm = columnElms[1].querySelector('.slick-resizable-handle') as HTMLDivElement;
const cMouseDownEvent = new CustomEvent('mousedown');
const bodyMouseMoveEvent = new CustomEvent('mousemove');
const bodyMouseUpEvent = new CustomEvent('mouseup');
Object.defineProperty(bodyMouseMoveEvent, 'target', { writable: true, value: resizeHandleElm });
Object.defineProperty(cMouseDownEvent, 'pageX', { writable: true, value: 9 });
Object.defineProperty(cMouseDownEvent, 'pageY', { writable: true, value: 12 });
Object.defineProperty(bodyMouseMoveEvent, 'pageX', { writable: true, value: -22 });
Object.defineProperty(bodyMouseMoveEvent, 'pageY', { writable: true, value: 13 });
// start resizing
resizeHandleElm.dispatchEvent(cMouseDownEvent);
container.dispatchEvent(cMouseDownEvent);
document.body.dispatchEvent(bodyMouseMoveEvent);
expect(columnElms[1].classList.contains('slick-header-column-active')).toBeTruthy();
expect(onColumnsDragSpy).toHaveBeenCalledWith({ triggeredByColumn: columnElms[1], resizeHandle: resizeHandleElm, grid }, expect.anything(), grid);
// header click won't get through
const onHeaderClickSpy = vi.spyOn(grid.onHeaderClick, 'notify');
container.querySelector('.slick-header')!.dispatchEvent(new CustomEvent('click'));
expect(onHeaderClickSpy).not.toHaveBeenCalled();
// end resizing
document.body.dispatchEvent(bodyMouseUpEvent);
vi.advanceTimersByTime(10);
expect(columnElms[1].classList.contains('slick-header-column-active')).toBeFalsy();
expect(onColumnsResizedSpy).toHaveBeenCalledWith({ triggeredByColumn: 'lastName', grid }, expect.anything(), grid);
expect(columns[0].width).toBe(80);
expect(columns[1].width).toBe(0);
expect(columns[2].width).toBe(65);
expect(columns[3].width).toBe(86);
expect(columns[4].width).toBe(80);
});
it('should not schedule resize auto-scroll timer in RTL when dragging outside viewport', () => {
grid = new SlickGrid(container, data, columns, {
...defaultOptions,
forceFitColumns: false,
rtl: true,
autoScrollOnColumnResize: true,
});
grid.init();
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
const columnElms = container.querySelectorAll('.slick-header-column');
const resizeHandleElm = columnElms[1].querySelector('.slick-resizable-handle') as HTMLDivElement;
const cMouseDownEvent = new CustomEvent('mousedown');
const bodyMouseMoveEvent = new CustomEvent('mousemove');
Object.defineProperty(bodyMouseMoveEvent, 'target', { writable: true, value: resizeHandleElm });
Object.defineProperty(cMouseDownEvent, 'pageX', { writable: true, value: 9 });
Object.defineProperty(cMouseDownEvent, 'pageY', { writable: true, value: 12 });
// Simulate dragging outside the viewport edge.
Object.defineProperty(bodyMouseMoveEvent, 'pageX', { writable: true, value: -200 });
Object.defineProperty(bodyMouseMoveEvent, 'pageY', { writable: true, value: 13 });
Object.defineProperty(bodyMouseMoveEvent, 'clientX', { writable: true, value: 0 });
resizeHandleElm.dispatchEvent(cMouseDownEvent);
container.dispatchEvent(cMouseDownEvent);
document.body.dispatchEvent(bodyMouseMoveEvent);
vi.advanceTimersByTime(120);
expect(setIntervalSpy).not.toHaveBeenCalled();
expect((grid as any)._columnResizeAutoScrollTimer).toBeUndefined();
setIntervalSpy.mockRestore();
});
});
});