diff --git a/demos/aurelia/src/examples/slickgrid/example57.html b/demos/aurelia/src/examples/slickgrid/example57.html
new file mode 100644
index 000000000..da89bf44f
--- /dev/null
+++ b/demos/aurelia/src/examples/slickgrid/example57.html
@@ -0,0 +1,24 @@
+
+ Example 57: RTL (Right-to-Left)
+
+
+ code
+
+
+
+
+Basic grid with RTL (Right-to-Left) enabled for RTL languages
+
+
diff --git a/demos/aurelia/src/examples/slickgrid/example57.ts b/demos/aurelia/src/examples/slickgrid/example57.ts
new file mode 100644
index 000000000..867aa840b
--- /dev/null
+++ b/demos/aurelia/src/examples/slickgrid/example57.ts
@@ -0,0 +1,78 @@
+import { Formatters, type Column, type GridOption } from 'aurelia-slickgrid';
+
+const NB_ITEMS = 100;
+
+export class Example57 {
+ gridOptions!: GridOption;
+ columns: Column[] = [];
+ dataset: any[] = [];
+ previousBodyDir: string | null = null;
+
+ constructor() {
+ this.defineGrid();
+ }
+
+ attached() {
+ this.previousBodyDir = document.body.getAttribute('dir');
+ document.body.setAttribute('dir', 'rtl');
+ this.dataset = this.mockData(NB_ITEMS);
+ }
+
+ detached() {
+ if (this.previousBodyDir) {
+ document.body.setAttribute('dir', this.previousBodyDir);
+ } else {
+ document.body.removeAttribute('dir');
+ }
+ }
+
+ defineGrid() {
+ this.columns = [
+ { id: 'id', name: 'ID', field: 'id', filterable: true, sortable: true, minWidth: 60 },
+ { id: 'title', name: 'Title', field: 'title', filterable: true, sortable: true, minWidth: 100 },
+ { id: 'duration', name: 'Duration (days)', field: 'duration', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ { id: '%', name: '% Complete', field: 'percentComplete', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ {
+ id: 'start',
+ name: 'Start',
+ field: 'start',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ {
+ id: 'finish',
+ name: 'Finish',
+ field: 'finish',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', minWidth: 80 },
+ ];
+
+ this.gridOptions = {
+ enableFiltering: true,
+ gridHeight: 500,
+ gridWidth: 700,
+ rowHeight: 33,
+ rtl: true, // ← Enable RTL mode
+ };
+ }
+
+ mockData(count: number) {
+ const data: any[] = [];
+ for (let i = 0; i < count; i++) {
+ data.push({
+ id: i,
+ title: `Task ${i}`,
+ duration: Math.round(Math.random() * 100),
+ percentComplete: Math.round(Math.random() * 100),
+ start: new Date(2024, 0, 1 + Math.floor(Math.random() * 30)).toISOString().split('T')[0],
+ finish: new Date(2024, 1, 1 + Math.floor(Math.random() * 28)).toISOString().split('T')[0],
+ effortDriven: i % 5 === 0,
+ });
+ }
+ return data;
+ }
+}
diff --git a/demos/aurelia/src/my-app.ts b/demos/aurelia/src/my-app.ts
index 3ce7d1a92..562a2e129 100644
--- a/demos/aurelia/src/my-app.ts
+++ b/demos/aurelia/src/my-app.ts
@@ -62,6 +62,7 @@ const myRoutes: Routeable[] = [
{ path: 'example54', component: () => import('./examples/slickgrid/example54.js'), title: '54- AI / Web MCP Toolkit' },
{ path: 'example55', component: () => import('./examples/slickgrid/example55.js'), title: '55- Variable Row Height (provider)' },
{ path: 'example56', component: () => import('./examples/slickgrid/example56.js'), title: '56- Variable Row Height (metadata)' },
+ { path: 'example57', component: () => import('./examples/slickgrid/example57.js'), title: '57- RTL (Right-to-Left)' },
{ path: 'home', component: () => import('./home-page.js'), title: 'Home' },
];
@route({
diff --git a/demos/aurelia/test/cypress/e2e/example57.cy.ts b/demos/aurelia/test/cypress/e2e/example57.cy.ts
new file mode 100644
index 000000000..df73fe98f
--- /dev/null
+++ b/demos/aurelia/test/cypress/e2e/example57.cy.ts
@@ -0,0 +1,81 @@
+describe('Example 57 - RTL (Right-to-Left)', () => {
+ const titles = ['ID', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
+
+ beforeEach(() => {
+ cy.setCookie('serve-mode', 'cypress');
+ cy.visit(`${Cypress.config('baseUrl')}/example57`);
+ });
+
+ describe('Basic Rendering', () => {
+ it('should display Example title', () => {
+ cy.get('h2').should('contain', 'Example 57: RTL (Right-to-Left)');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Configuration', () => {
+ it('should have RTL class applied to grid container', () => {
+ cy.get('#grid57').then(($grid) => {
+ const target = $grid.hasClass('slickgrid-container') ? $grid : $grid.find('.slickgrid-container');
+ cy.wrap(target).should('have.class', 'slick-rtl');
+ });
+ });
+
+ it('should have proper RTL cell content alignment', () => {
+ cy.get('#grid57 .slick-cell:first').should('have.css', 'direction', 'rtl');
+ });
+ });
+
+ describe('UI Interactions', () => {
+ it('should have resize handle on the left side', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle').should('exist').and('have.css', 'left', '0px');
+ });
+
+ it('should maintain RTL column order after resize', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle')
+ .trigger('mousedown', { which: 1 })
+ .then(() => {
+ cy.get('body').trigger('mousemove', { clientX: 260, clientY: 0 });
+ cy.get('body').trigger('mouseup');
+ });
+
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Scrolling Behavior', () => {
+ it('should have horizontal scroll enabled', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(viewport.scrollWidth).to.be.greaterThan(viewport.clientWidth);
+ });
+ });
+
+ it('should update visible header columns when scrolling', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = maxScroll;
+ if (viewport.scrollLeft === 0) {
+ viewport.scrollLeft = -maxScroll;
+ }
+ });
+
+ cy.wait(150);
+
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(Math.abs(viewport.scrollLeft)).to.be.greaterThan(0);
+ });
+ });
+ });
+});
diff --git a/demos/react/src/examples/slickgrid/App.tsx b/demos/react/src/examples/slickgrid/App.tsx
index 7fe024808..f44e0121f 100644
--- a/demos/react/src/examples/slickgrid/App.tsx
+++ b/demos/react/src/examples/slickgrid/App.tsx
@@ -58,6 +58,7 @@ const routes = [
{ path: 'example54', route: '/example54', element: lazy(() => import('./Example54.js')), title: '54- AI / Web MCP Toolkit' },
{ path: 'example55', route: '/example55', element: lazy(() => import('./Example55.js')), title: '55- Variable Row Height (provider)' },
{ path: 'example56', route: '/example56', element: lazy(() => import('./Example56.js')), title: '56- Variable Row Height (metadata)' },
+ { path: 'example57', route: '/example57', element: lazy(() => import('./Example57.js')), title: '57- RTL (Right-to-Left)' },
];
export default function Routes() {
diff --git a/demos/react/src/examples/slickgrid/Example57.tsx b/demos/react/src/examples/slickgrid/Example57.tsx
new file mode 100644
index 000000000..1ec7ff431
--- /dev/null
+++ b/demos/react/src/examples/slickgrid/Example57.tsx
@@ -0,0 +1,104 @@
+import React, { useEffect, useState } from 'react';
+import { Formatters, SlickgridReact, type Column, type GridOption } from 'slickgrid-react';
+
+const NB_ITEMS = 100;
+
+const Example57: React.FC = () => {
+ const [gridOptions, setGridOptions] = useState(undefined);
+ const [columns, setColumns] = useState([]);
+ const [dataset, setDataset] = useState([]);
+
+ useEffect(() => {
+ const previousBodyDir = document.body.getAttribute('dir');
+ document.body.setAttribute('dir', 'rtl');
+
+ defineGrid();
+ const mockData = mockDataset();
+ setDataset(mockData);
+
+ return () => {
+ if (previousBodyDir) {
+ document.body.setAttribute('dir', previousBodyDir);
+ } else {
+ document.body.removeAttribute('dir');
+ }
+ };
+ }, []);
+
+ const defineGrid = () => {
+ const cols: Column[] = [
+ { id: 'id', name: 'ID', field: 'id', filterable: true, sortable: true, minWidth: 60 },
+ { id: 'title', name: 'Title', field: 'title', filterable: true, sortable: true, minWidth: 100 },
+ { id: 'duration', name: 'Duration (days)', field: 'duration', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ { id: '%', name: '% Complete', field: 'percentComplete', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ {
+ id: 'start',
+ name: 'Start',
+ field: 'start',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ {
+ id: 'finish',
+ name: 'Finish',
+ field: 'finish',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', minWidth: 80 },
+ ];
+ setColumns(cols);
+
+ const opts: GridOption = {
+ enableFiltering: true,
+ gridHeight: 500,
+ gridWidth: 700,
+ rowHeight: 33,
+ rtl: true, // ← Enable RTL mode
+ };
+ setGridOptions(opts);
+ };
+
+ const mockDataset = () => {
+ const data = [];
+ for (let i = 0; i < NB_ITEMS; i++) {
+ data.push({
+ id: i,
+ title: `Task ${i}`,
+ duration: Math.round(Math.random() * 100),
+ percentComplete: Math.round(Math.random() * 100),
+ start: new Date(2024, 0, 1 + Math.floor(Math.random() * 30)).toISOString().split('T')[0],
+ finish: new Date(2024, 1, 1 + Math.floor(Math.random() * 28)).toISOString().split('T')[0],
+ effortDriven: i % 5 === 0,
+ });
+ }
+ return data;
+ };
+
+ return !gridOptions ? null : (
+
+
+ Example 57: RTL (Right-to-Left)
+
+ see
+
+ code
+
+
+
+
+
Basic grid with RTL (Right-to-Left) enabled for RTL languages.
+
+
+
+
+
+ );
+};
+
+export default Example57;
diff --git a/demos/react/test/cypress/e2e/example57.cy.ts b/demos/react/test/cypress/e2e/example57.cy.ts
new file mode 100644
index 000000000..df73fe98f
--- /dev/null
+++ b/demos/react/test/cypress/e2e/example57.cy.ts
@@ -0,0 +1,81 @@
+describe('Example 57 - RTL (Right-to-Left)', () => {
+ const titles = ['ID', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
+
+ beforeEach(() => {
+ cy.setCookie('serve-mode', 'cypress');
+ cy.visit(`${Cypress.config('baseUrl')}/example57`);
+ });
+
+ describe('Basic Rendering', () => {
+ it('should display Example title', () => {
+ cy.get('h2').should('contain', 'Example 57: RTL (Right-to-Left)');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Configuration', () => {
+ it('should have RTL class applied to grid container', () => {
+ cy.get('#grid57').then(($grid) => {
+ const target = $grid.hasClass('slickgrid-container') ? $grid : $grid.find('.slickgrid-container');
+ cy.wrap(target).should('have.class', 'slick-rtl');
+ });
+ });
+
+ it('should have proper RTL cell content alignment', () => {
+ cy.get('#grid57 .slick-cell:first').should('have.css', 'direction', 'rtl');
+ });
+ });
+
+ describe('UI Interactions', () => {
+ it('should have resize handle on the left side', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle').should('exist').and('have.css', 'left', '0px');
+ });
+
+ it('should maintain RTL column order after resize', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle')
+ .trigger('mousedown', { which: 1 })
+ .then(() => {
+ cy.get('body').trigger('mousemove', { clientX: 260, clientY: 0 });
+ cy.get('body').trigger('mouseup');
+ });
+
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Scrolling Behavior', () => {
+ it('should have horizontal scroll enabled', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(viewport.scrollWidth).to.be.greaterThan(viewport.clientWidth);
+ });
+ });
+
+ it('should update visible header columns when scrolling', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = maxScroll;
+ if (viewport.scrollLeft === 0) {
+ viewport.scrollLeft = -maxScroll;
+ }
+ });
+
+ cy.wait(150);
+
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(Math.abs(viewport.scrollLeft)).to.be.greaterThan(0);
+ });
+ });
+ });
+});
diff --git a/demos/vanilla/src/app-routing.ts b/demos/vanilla/src/app-routing.ts
index c705c59a9..1976666f5 100644
--- a/demos/vanilla/src/app-routing.ts
+++ b/demos/vanilla/src/app-routing.ts
@@ -43,6 +43,7 @@ import Example42 from './examples/example42.js';
import Example43 from './examples/example43.js';
import Example44 from './examples/example44.js';
import Example45 from './examples/example45.js';
+import Example46 from './examples/example46.js';
import Icons from './examples/icons.js';
import type { RouterConfig } from './interfaces.js';
@@ -96,6 +97,7 @@ export class AppRouting {
{ route: 'example43', name: 'example43', view: './examples/example43.html', viewModel: Example43, title: 'Example43' },
{ route: 'example44', name: 'example44', view: './examples/example44.html', viewModel: Example44, title: 'Example44' },
{ route: 'example45', name: 'example45', view: './examples/example45.html', viewModel: Example45, title: 'Example45' },
+ { route: 'example46', name: 'example46', view: './examples/example46.html', viewModel: Example46, title: 'Example46' },
{ route: '', redirect: 'example01' },
{ route: '**', redirect: 'example01' },
];
diff --git a/demos/vanilla/src/app.html b/demos/vanilla/src/app.html
index 4482f40cf..8756cc7cb 100644
--- a/demos/vanilla/src/app.html
+++ b/demos/vanilla/src/app.html
@@ -29,8 +29,7 @@ Slickgrid-Universal
Documentation
SlickGrid Icons
diff --git a/demos/vanilla/src/examples/example46.html b/demos/vanilla/src/examples/example46.html
new file mode 100644
index 000000000..4bf26370b
--- /dev/null
+++ b/demos/vanilla/src/examples/example46.html
@@ -0,0 +1,22 @@
+
+
+ Example 46 - RTL (Right-to-Left)
+ with column resizing
+
+
+
+
Basic grid with RTL (Right-to-Left) enabled for RTL languages
+
+
+
diff --git a/demos/vanilla/src/examples/example46.scss b/demos/vanilla/src/examples/example46.scss
new file mode 100644
index 000000000..3b29bebb5
--- /dev/null
+++ b/demos/vanilla/src/examples/example46.scss
@@ -0,0 +1,8 @@
+.grid46 {
+ direction: rtl;
+ --slick-header-menu-display: inline-block;
+}
+
+.demo-container.grid46 {
+ inset-inline-start: 50px;
+}
diff --git a/demos/vanilla/src/examples/example46.ts b/demos/vanilla/src/examples/example46.ts
new file mode 100644
index 000000000..938a5715b
--- /dev/null
+++ b/demos/vanilla/src/examples/example46.ts
@@ -0,0 +1,88 @@
+import { Formatters, type Column, type GridOption } from '@slickgrid-universal/common';
+import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
+import { ExampleGridOptions } from './example-grid-options.js';
+import './example46.scss';
+
+const NB_ITEMS = 100;
+
+export default class Example46 {
+ gridOptions!: GridOption;
+ columns!: Column[];
+ dataset!: any[];
+ sgb!: SlickVanillaGridBundle;
+ previousBodyDir: string | null = null;
+
+ attached() {
+ this.previousBodyDir = document.body.getAttribute('dir');
+ document.body.setAttribute('dir', 'rtl');
+
+ this.defineGrid();
+ this.dataset = this.mockData(NB_ITEMS);
+
+ this.sgb = new Slicker.GridBundle(
+ document.querySelector('.grid46') as HTMLDivElement,
+ this.columns,
+ { ...ExampleGridOptions, ...this.gridOptions },
+ this.dataset
+ );
+ }
+
+ dispose() {
+ this.sgb?.dispose();
+ if (this.previousBodyDir) {
+ document.body.setAttribute('dir', this.previousBodyDir);
+ } else {
+ document.body.removeAttribute('dir');
+ }
+ }
+
+ defineGrid() {
+ this.columns = [
+ { id: 'id', name: 'ID', field: 'id', filterable: true, sortable: true, minWidth: 60 },
+ { id: 'title', name: 'Title', field: 'title', filterable: true, sortable: true, minWidth: 100 },
+ { id: 'duration', name: 'Duration (days)', field: 'duration', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ { id: '%', name: '% Complete', field: 'percentComplete', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ {
+ id: 'start',
+ name: 'Start',
+ field: 'start',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ {
+ id: 'finish',
+ name: 'Finish',
+ field: 'finish',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', minWidth: 80 },
+ ];
+
+ this.gridOptions = {
+ enableFiltering: true,
+ gridHeight: 500,
+ gridWidth: 900,
+ rowHeight: 33,
+ rtl: true, // ← Enable RTL mode
+ };
+ }
+
+ mockData(count: number) {
+ const data: any[] = [];
+ for (let i = 0; i < count; i++) {
+ data.push({
+ id: i,
+ title: `Task ${i}`,
+ duration: Math.round(Math.random() * 100),
+ percentComplete: Math.round(Math.random() * 100),
+ start: new Date(2024, 0, 1 + Math.floor(Math.random() * 30)).toISOString().split('T')[0],
+ finish: new Date(2024, 1, 1 + Math.floor(Math.random() * 28)).toISOString().split('T')[0],
+ effortDriven: i % 5 === 0,
+ });
+ }
+ return data;
+ }
+}
diff --git a/demos/vue/src/components/Example57.vue b/demos/vue/src/components/Example57.vue
new file mode 100644
index 000000000..b83f68da9
--- /dev/null
+++ b/demos/vue/src/components/Example57.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+ Example 57: RTL (Right-to-Left)
+
+
+ code
+
+
+
+
Basic grid with RTL (Right-to-Left) enabled for RTL languages
+
+
+
+
+
+
diff --git a/demos/vue/src/router/index.ts b/demos/vue/src/router/index.ts
index c6f8e0f47..c5973a1de 100644
--- a/demos/vue/src/router/index.ts
+++ b/demos/vue/src/router/index.ts
@@ -64,6 +64,7 @@ export const routes: RouteRecordRaw[] = [
{ path: '/example54', name: '54- AI / Web MCP Toolkit', component: () => import('../components/Example54.vue') },
{ path: '/example55', name: '55- Variable Row Height (provider)', component: () => import('../components/Example55.vue') },
{ path: '/example56', name: '56- Variable Row Height (metadata)', component: () => import('../components/Example56.vue') },
+ { path: '/example57', name: '57- RTL (Right-to-Left)', component: () => import('../components/Example57.vue') },
];
export const router = createRouter({
diff --git a/demos/vue/test/cypress/e2e/example57.cy.ts b/demos/vue/test/cypress/e2e/example57.cy.ts
new file mode 100644
index 000000000..df73fe98f
--- /dev/null
+++ b/demos/vue/test/cypress/e2e/example57.cy.ts
@@ -0,0 +1,81 @@
+describe('Example 57 - RTL (Right-to-Left)', () => {
+ const titles = ['ID', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
+
+ beforeEach(() => {
+ cy.setCookie('serve-mode', 'cypress');
+ cy.visit(`${Cypress.config('baseUrl')}/example57`);
+ });
+
+ describe('Basic Rendering', () => {
+ it('should display Example title', () => {
+ cy.get('h2').should('contain', 'Example 57: RTL (Right-to-Left)');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Configuration', () => {
+ it('should have RTL class applied to grid container', () => {
+ cy.get('#grid57').then(($grid) => {
+ const target = $grid.hasClass('slickgrid-container') ? $grid : $grid.find('.slickgrid-container');
+ cy.wrap(target).should('have.class', 'slick-rtl');
+ });
+ });
+
+ it('should have proper RTL cell content alignment', () => {
+ cy.get('#grid57 .slick-cell:first').should('have.css', 'direction', 'rtl');
+ });
+ });
+
+ describe('UI Interactions', () => {
+ it('should have resize handle on the left side', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle').should('exist').and('have.css', 'left', '0px');
+ });
+
+ it('should maintain RTL column order after resize', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle')
+ .trigger('mousedown', { which: 1 })
+ .then(() => {
+ cy.get('body').trigger('mousemove', { clientX: 260, clientY: 0 });
+ cy.get('body').trigger('mouseup');
+ });
+
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Scrolling Behavior', () => {
+ it('should have horizontal scroll enabled', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(viewport.scrollWidth).to.be.greaterThan(viewport.clientWidth);
+ });
+ });
+
+ it('should update visible header columns when scrolling', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = maxScroll;
+ if (viewport.scrollLeft === 0) {
+ viewport.scrollLeft = -maxScroll;
+ }
+ });
+
+ cy.wait(150);
+
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(Math.abs(viewport.scrollLeft)).to.be.greaterThan(0);
+ });
+ });
+ });
+});
diff --git a/frameworks/angular-slickgrid/src/demos/app-routing.module.ts b/frameworks/angular-slickgrid/src/demos/app-routing.module.ts
index 945ede054..ec0a73b75 100644
--- a/frameworks/angular-slickgrid/src/demos/app-routing.module.ts
+++ b/frameworks/angular-slickgrid/src/demos/app-routing.module.ts
@@ -58,6 +58,7 @@ export const routes: Routes = [
{ path: 'example54', loadComponent: () => import('./examples/example54.component').then((m) => m.Example54Component) },
{ path: 'example55', loadComponent: () => import('./examples/example55.component').then((m) => m.Example55Component) },
{ path: 'example56', loadComponent: () => import('./examples/example56.component').then((m) => m.Example56Component) },
+ { path: 'example57', loadComponent: () => import('./examples/example57.component').then((m) => m.Example57Component) },
{ path: '', redirectTo: '/example34', pathMatch: 'full' },
{ path: '**', redirectTo: '/example34', pathMatch: 'full' },
];
diff --git a/frameworks/angular-slickgrid/src/demos/app.component.html b/frameworks/angular-slickgrid/src/demos/app.component.html
index c9e6c2a73..a9a4a5b44 100644
--- a/frameworks/angular-slickgrid/src/demos/app.component.html
+++ b/frameworks/angular-slickgrid/src/demos/app.component.html
@@ -212,6 +212,9 @@
56- Variable Row Height (metadata)
+
+ 57- RTL (Right-to-Left)
+
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example57.component.html b/frameworks/angular-slickgrid/src/demos/examples/example57.component.html
new file mode 100644
index 000000000..2084b3e6c
--- /dev/null
+++ b/frameworks/angular-slickgrid/src/demos/examples/example57.component.html
@@ -0,0 +1,19 @@
+
+
+ Example 57: RTL (Right-to-Left)
+
+
+ code
+
+
+
+
Basic grid with RTL (Right-to-Left) enabled for RTL languages
+
+
+
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example57.component.scss b/frameworks/angular-slickgrid/src/demos/examples/example57.component.scss
new file mode 100644
index 000000000..b52790d72
--- /dev/null
+++ b/frameworks/angular-slickgrid/src/demos/examples/example57.component.scss
@@ -0,0 +1,3 @@
+.grid-rtl {
+ direction: rtl;
+}
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example57.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example57.component.ts
new file mode 100644
index 000000000..0bac8bca1
--- /dev/null
+++ b/frameworks/angular-slickgrid/src/demos/examples/example57.component.ts
@@ -0,0 +1,89 @@
+import { Component, type OnDestroy, type OnInit } from '@angular/core';
+import { AngularSlickgridComponent, Formatters, type Column, type GridOption } from '../../library';
+
+const NB_ITEMS = 100;
+
+@Component({
+ templateUrl: './example57.component.html',
+ styleUrls: ['./example57.component.scss'],
+ imports: [AngularSlickgridComponent],
+})
+export class Example57Component implements OnInit, OnDestroy {
+ columns: Column[] = [];
+ gridOptions!: GridOption;
+ dataset!: any[];
+ hideSubTitle = false;
+ previousBodyDir: string | null = null;
+
+ ngOnInit(): void {
+ this.previousBodyDir = document.body.getAttribute('dir');
+ document.body.setAttribute('dir', 'rtl');
+
+ this.prepareGrid();
+ this.dataset = this.mockData(NB_ITEMS);
+ }
+
+ ngOnDestroy(): void {
+ if (this.previousBodyDir) {
+ document.body.setAttribute('dir', this.previousBodyDir);
+ } else {
+ document.body.removeAttribute('dir');
+ }
+ }
+
+ prepareGrid() {
+ this.columns = [
+ { id: 'id', name: 'ID', field: 'id', filterable: true, sortable: true, minWidth: 60 },
+ { id: 'title', name: 'Title', field: 'title', filterable: true, sortable: true, minWidth: 100 },
+ { id: 'duration', name: 'Duration (days)', field: 'duration', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ { id: '%', name: '% Complete', field: 'percentComplete', filterable: true, sortable: true, minWidth: 100, type: 'number' },
+ {
+ id: 'start',
+ name: 'Start',
+ field: 'start',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ {
+ id: 'finish',
+ name: 'Finish',
+ field: 'finish',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true,
+ filterable: true,
+ },
+ { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', minWidth: 80 },
+ ];
+
+ this.gridOptions = {
+ enableFiltering: true,
+ gridHeight: 500,
+ gridWidth: 700,
+ rowHeight: 33,
+ rtl: true, // ← Enable RTL mode
+ };
+ }
+
+ mockData(count: number) {
+ const data = [];
+ for (let i = 0; i < count; i++) {
+ data.push({
+ id: i,
+ title: `Task ${i}`,
+ duration: Math.round(Math.random() * 100),
+ percentComplete: Math.round(Math.random() * 100),
+ start: new Date(2024, 0, 1 + Math.floor(Math.random() * 30)).toISOString().split('T')[0],
+ finish: new Date(2024, 1, 1 + Math.floor(Math.random() * 28)).toISOString().split('T')[0],
+ effortDriven: i % 5 === 0,
+ });
+ }
+ return data;
+ }
+
+ toggleSubTitle() {
+ this.hideSubTitle = !this.hideSubTitle;
+ const action = this.hideSubTitle ? 'add' : 'remove';
+ document.querySelector('.subtitle')?.classList[action]('hidden');
+ }
+}
diff --git a/frameworks/angular-slickgrid/test/cypress/e2e/example57.cy.ts b/frameworks/angular-slickgrid/test/cypress/e2e/example57.cy.ts
new file mode 100644
index 000000000..df73fe98f
--- /dev/null
+++ b/frameworks/angular-slickgrid/test/cypress/e2e/example57.cy.ts
@@ -0,0 +1,81 @@
+describe('Example 57 - RTL (Right-to-Left)', () => {
+ const titles = ['ID', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
+
+ beforeEach(() => {
+ cy.setCookie('serve-mode', 'cypress');
+ cy.visit(`${Cypress.config('baseUrl')}/example57`);
+ });
+
+ describe('Basic Rendering', () => {
+ it('should display Example title', () => {
+ cy.get('h2').should('contain', 'Example 57: RTL (Right-to-Left)');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Configuration', () => {
+ it('should have RTL class applied to grid container', () => {
+ cy.get('#grid57').then(($grid) => {
+ const target = $grid.hasClass('slickgrid-container') ? $grid : $grid.find('.slickgrid-container');
+ cy.wrap(target).should('have.class', 'slick-rtl');
+ });
+ });
+
+ it('should have proper RTL cell content alignment', () => {
+ cy.get('#grid57 .slick-cell:first').should('have.css', 'direction', 'rtl');
+ });
+ });
+
+ describe('UI Interactions', () => {
+ it('should have resize handle on the left side', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle').should('exist').and('have.css', 'left', '0px');
+ });
+
+ it('should maintain RTL column order after resize', () => {
+ cy.get('#grid57 .slick-header-column:first .slick-resizable-handle')
+ .trigger('mousedown', { which: 1 })
+ .then(() => {
+ cy.get('body').trigger('mousemove', { clientX: 260, clientY: 0 });
+ cy.get('body').trigger('mouseup');
+ });
+
+ cy.get('#grid57')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Scrolling Behavior', () => {
+ it('should have horizontal scroll enabled', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(viewport.scrollWidth).to.be.greaterThan(viewport.clientWidth);
+ });
+ });
+
+ it('should update visible header columns when scrolling', () => {
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = maxScroll;
+ if (viewport.scrollLeft === 0) {
+ viewport.scrollLeft = -maxScroll;
+ }
+ });
+
+ cy.wait(150);
+
+ cy.get('#grid57 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(Math.abs(viewport.scrollLeft)).to.be.greaterThan(0);
+ });
+ });
+ });
+});
diff --git a/packages/common/src/core/__tests__/slickGrid-rtl.spec.ts b/packages/common/src/core/__tests__/slickGrid-rtl.spec.ts
new file mode 100644
index 000000000..deee4c79d
--- /dev/null
+++ b/packages/common/src/core/__tests__/slickGrid-rtl.spec.ts
@@ -0,0 +1,255 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Column, GridOption } from '../../interfaces/index.js';
+import { SlickEventData } from '../slickCore.js';
+import { SlickGrid } from '../slickGrid.js';
+
+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,
+ devMode: { ownerNodeIndex: 0 },
+ };
+ 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 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(600);
+ expect(range.rightPx).toBe(1400);
+ 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 frozen columns', () => {
+ const gridContainer = document.getElementById(gridId) as HTMLElement;
+ grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true, frozenColumn: 0 });
+ expect(grid.getOptions().rtl).toBe(true);
+ expect(grid.getOptions().frozenColumn).toBe(0);
+ });
+
+ it('should support RTL with sorting', () => {
+ const gridContainer = document.getElementById(gridId) as HTMLElement;
+ grid = new SlickGrid(gridContainer, items, columns, { ...defaultOptions, rtl: true, enableSorting: 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, enableFiltering: 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();
+ });
+ });
+});
diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts
index 8f930996a..71062b07d 100755
--- a/packages/common/src/core/slickGrid.ts
+++ b/packages/common/src/core/slickGrid.ts
@@ -315,6 +315,7 @@ export class SlickGrid = Column, O e
enableMouseWheelScrollHandler: true,
doPaging: true,
rowTopOffsetRenderType: 'top',
+ rtl: false,
scrollRenderThrottling: 10,
suppressCssChangesOnHiddenInit: false,
ffMaxSupportedCssHeight: 6000000,
@@ -787,12 +788,12 @@ export class SlickGrid = Column, O e
// Append the columnn containers to the headers
this._headerL = createDomElement(
'div',
- { className: 'slick-header-columns slick-header-columns-left', style: { left: '-1000px' }, role: 'row' },
+ { className: 'slick-header-columns slick-header-columns-left', style: { [this.dirSide]: '-1000px' }, role: 'row' },
this._headerScrollerL
);
this._headerR = createDomElement(
'div',
- { className: 'slick-header-columns slick-header-columns-right', style: { left: '-1000px' }, role: 'row' },
+ { className: 'slick-header-columns slick-header-columns-right', style: { [this.dirSide]: '-1000px' }, role: 'row' },
this._headerScrollerR
);
@@ -969,6 +970,8 @@ export class SlickGrid = Column, O e
if (!this._options.explicitInitialization) {
this.finishInitialization();
}
+
+ this.applyRTL(this._options.rtl ?? false);
}
protected finishInitialization(): void {
@@ -2321,6 +2324,13 @@ export class SlickGrid = Column, O e
targetPageX: number,
resizeCallback: (targetPageX: number) => void
) => {
+ // TODO: there is a known bug with auto-scroll in RTL,
+ // so disable it until someone can contribute a fix
+ if (this._options.rtl) {
+ stopColumnResizeAutoScroll();
+ return;
+ }
+
autoScrollClientX = isDefinedNumber(clientX) ? clientX : autoScrollClientX;
const viewportOffset = getOffset(this._viewportScrollContainerX);
const left = viewportOffset.left;
@@ -2377,7 +2387,12 @@ export class SlickGrid = Column, O e
) => {
this.columnResizeDragging = true;
let actualMinWidth;
- const d = Math.min(maxPageX, Math.max(minPageX, targetPageX)) - pageX;
+ let d = Math.min(maxPageX, Math.max(minPageX, targetPageX)) - pageX;
+
+ if (this._options.rtl) {
+ d = -d;
+ }
+
let x;
let newCanvasWidthL = 0;
// oxlint-disable-next-line no-unused-vars
@@ -2556,6 +2571,7 @@ export class SlickGrid = Column, O e
this.updateCanvasWidth();
if (
this._options.autoScrollOnColumnResize &&
+ !this._options.rtl &&
!this._options.forceFitColumns &&
!(this.hasFrozenColumns() && i <= this._options.frozenColumn!)
) {
@@ -2636,8 +2652,13 @@ export class SlickGrid = Column, O e
shrinkLeewayOnLeft += (c.previousWidth || 0) - Math.max(c.minWidth || 0, this.absoluteColumnMinWidth);
}
}
- maxPageX = pageX + Math.min(shrinkLeewayOnRight ?? 100000, stretchLeewayOnLeft ?? 100000);
- minPageX = pageX - Math.min(shrinkLeewayOnLeft ?? 100000, stretchLeewayOnRight ?? 100000);
+ if (this._options.rtl) {
+ maxPageX = pageX + Math.min(shrinkLeewayOnLeft ?? 100000, stretchLeewayOnRight ?? 100000);
+ minPageX = pageX - Math.min(shrinkLeewayOnRight ?? 100000, stretchLeewayOnLeft ?? 100000);
+ } else {
+ maxPageX = pageX + Math.min(shrinkLeewayOnRight ?? 100000, stretchLeewayOnLeft ?? 100000);
+ minPageX = pageX - Math.min(shrinkLeewayOnLeft ?? 100000, stretchLeewayOnRight ?? 100000);
+ }
resizeAutoScrollDeltaX = 0;
autoScrollClientX = isDefinedNumber((targetEvent as MouseEvent).clientX) ? (targetEvent as MouseEvent).clientX : undefined;
stopColumnResizeAutoScroll();
@@ -2919,8 +2940,8 @@ export class SlickGrid = Column, O e
(this._options.shadowRoot || document.head).appendChild(this._style);
const rules = [
- `.${this.uid} .slick-group-header-column { left: 1000px; }`,
- `.${this.uid} .slick-header-column { left: 1000px; }`,
+ `.${this.uid} .slick-group-header-column { ${this.dirSide}: 1000px; }`,
+ `.${this.uid} .slick-header-column { ${this.dirSide}: 1000px; }`,
`.${this.uid} .slick-top-panel { height: ${this._options.topPanelHeight}px; }`,
`.${this.uid} .slick-preheader-panel { height: ${this._options.preHeaderPanelHeight}px; }`,
`.${this.uid} .slick-topheader-panel { height: ${this._options.topHeaderPanelHeight}px; }`,
@@ -3331,12 +3352,22 @@ export class SlickGrid = Column, O e
w = this.columns[i].hidden ? 0 : this.columns[i].width || 0;
rule = this.getColumnCssRules(i);
- if (rule.left) {
- rule.left.style.left = `${x}px`;
- }
- if (rule.right) {
- rule.right.style.right =
- (this._options.frozenColumn !== -1 && i > this._options.frozenColumn! ? this.canvasWidthR : this.canvasWidthL) - x - w + 'px';
+ if (this._options.rtl) {
+ if (rule.left) {
+ rule.left.style.right = `${x}px`;
+ }
+ if (rule.right) {
+ rule.right.style.left =
+ (this._options.frozenColumn !== -1 && i > this._options.frozenColumn! ? this.canvasWidthR : this.canvasWidthL) - x - w + 'px';
+ }
+ } else {
+ if (rule.left) {
+ rule.left.style.left = `${x}px`;
+ }
+ if (rule.right) {
+ rule.right.style.right =
+ (this._options.frozenColumn !== -1 && i > this._options.frozenColumn! ? this.canvasWidthR : this.canvasWidthL) - x - w + 'px';
+ }
}
// If this column is frozen, reset the css left value since the
@@ -5259,11 +5290,21 @@ export class SlickGrid = Column, O e
viewportTop ??= this.scrollTop;
viewportLeft ??= this.scrollLeft;
+ let leftPx = viewportLeft;
+ let rightPx = viewportLeft + this.viewportW;
+
+ if (this._options.rtl) {
+ // In RTL mode, scrollLeft is the offset from the right edge.
+ const maxScroll = this.canvasWidth - this.viewportW;
+ leftPx = maxScroll - viewportLeft - this.viewportW;
+ rightPx = maxScroll - viewportLeft;
+ }
+
return {
top: this.getRowFromPosition(viewportTop),
bottom: this.getRowFromPosition(viewportTop + this.viewportH) + 1,
- leftPx: viewportLeft,
- rightPx: viewportLeft + this.viewportW,
+ leftPx,
+ rightPx,
};
}
@@ -5756,7 +5797,7 @@ export class SlickGrid = Column, O e
protected _handleScroll(eventType: 'mousewheel' | 'scroll' | 'system' = 'system'): boolean {
let maxScrollDistanceY = this._viewportScrollContainerY.scrollHeight - this._viewportScrollContainerY.clientHeight;
- let maxScrollDistanceX = this._viewportScrollContainerY.scrollWidth - this._viewportScrollContainerY.clientWidth;
+ let maxScrollDistanceX = this._viewportScrollContainerX.scrollWidth - this._viewportScrollContainerX.clientWidth;
// Protect against erroneous clientHeight/Width greater than scrollHeight/Width.
// Sometimes seen in Chrome.
@@ -8128,4 +8169,27 @@ export class SlickGrid = Column, O e
sanitizeHtmlString(dirtyHtml: unknown): T {
return runOptionalHtmlSanitizer(dirtyHtml, this._options?.sanitizer);
}
+
+ /**
+ * Returns the CSS property used to hide header columns off-screen by applying a large offset (e.g., `1000px`).
+ *
+ * In LTR mode (`rtl: false`), columns are positioned with a negative `left` value to hide them off-screen.
+ * In RTL mode (`rtl: true`), the same effect is achieved by using a positive `right` value, since the scroll direction is mirrored.
+ *
+ * @returns 'right' when RTL is enabled, otherwise 'left'
+ */
+ protected get dirSide(): string {
+ return this._options.rtl ? 'right' : 'left';
+ }
+
+ /** Applies/removes RTL state directly on the grid container. */
+ private applyRTL(enabled: boolean): void {
+ if (enabled) {
+ this._container.classList.add('slick-rtl');
+ this._container.setAttribute('dir', 'rtl');
+ } else {
+ this._container.classList.remove('slick-rtl');
+ this._container.removeAttribute('dir');
+ }
+ }
}
diff --git a/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts b/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts
index 3208c0ccd..ceec305b5 100644
--- a/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts
+++ b/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts
@@ -149,6 +149,7 @@ describe('GridMenuControl', () => {
enableAutoSizeColumns: true,
enableGridMenu: true,
enableTranslate: true,
+ rtl: false,
backendServiceApi: {
service: {
buildQuery: vi.fn(),
@@ -2792,5 +2793,15 @@ describe('GridMenuControl', () => {
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
});
+
+ it('should open grid menu on the right when using RTL', () => {
+ gridOptionsMock.rtl = true;
+ control.init();
+ const buttonElm = document.querySelector('.slick-grid-menu-button') as HTMLDivElement;
+ buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const gridMenuElm = document.querySelector('.slick-grid-menu') as HTMLDivElement;
+
+ expect(gridMenuElm.classList.contains('dropright')).toBe(true);
+ });
});
});
diff --git a/packages/common/src/extensions/slickGridMenu.ts b/packages/common/src/extensions/slickGridMenu.ts
index 2717a6e89..02f6b2335 100644
--- a/packages/common/src/extensions/slickGridMenu.ts
+++ b/packages/common/src/extensions/slickGridMenu.ts
@@ -146,6 +146,12 @@ export class SlickGridMenu extends MenuBaseClass {
}
this._userOriginalGridMenu = { ...this.sharedService.gridOptions.gridMenu };
this._addonOptions = { ...this._defaults, ...this.getDefaultGridMenuOptions(), ...this.sharedService.gridOptions.gridMenu };
+
+ // adjust dropSide for RTL mode (menu should open to the right when button is on the left in RTL)
+ if (this.sharedService.gridOptions.rtl) {
+ this._addonOptions.dropSide = 'right';
+ }
+
this.sharedService.gridOptions.gridMenu = this._addonOptions;
// merge original user grid menu items with internal items
diff --git a/packages/common/src/interfaces/gridOption.interface.ts b/packages/common/src/interfaces/gridOption.interface.ts
index 8db2cbd7a..5f5c2fa61 100644
--- a/packages/common/src/interfaces/gridOption.interface.ts
+++ b/packages/common/src/interfaces/gridOption.interface.ts
@@ -863,6 +863,9 @@ export interface GridOption {
/** Defaults to 400, duration to show the row highlight (e.g. after insert/edit/...) */
rowHighlightDuration?: number;
+ /** Defaults to false, sets the grid direction to RTL (Right-to-Left) for proper rendering of RTL languages */
+ rtl?: boolean;
+
/** Row Move Manager Plugin options & events */
rowMoveManager?: RowMoveManager;
diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss
index dd5e72a37..f36316aae 100644
--- a/packages/common/src/styles/_variables.scss
+++ b/packages/common/src/styles/_variables.scss
@@ -359,7 +359,7 @@ $slick-column-picker-item-hover-border: 1px solid #d5d5d5 !d
$slick-column-picker-item-hover-color: #fafafa !default;
$slick-column-picker-label-margin: 4px !default;
$slick-column-picker-label-font-weight: normal !default;
-$slick-column-picker-label-text-padding-left: 4px !default;
+$slick-column-picker-label-gap: 4px !default;
$slick-column-picker-link-background-color: #ffffff !default;
$slick-column-picker-list-margin-bottom: 8px !default;
$slick-column-picker-opacity-hover: 0.45 !default;
@@ -407,6 +407,7 @@ $slick-menu-item-border: 1px solid transparen
$slick-menu-item-border-radius: 0px !default;
$slick-menu-item-disabled-color: silver !default;
$slick-menu-item-font-size: $slick-font-size-base !default;
+$slick-menu-item-gap: 4px !default;
$slick-menu-item-height: 28px !default;
$slick-menu-item-hover-border: 1px solid #d5d5d5 !default;
$slick-menu-item-hover-color: #fafafa !default;
@@ -416,7 +417,6 @@ $slick-menu-item-white-space: nowrap !default;
$slick-menu-icon-font-size: $slick-icon-font-size !default;
$slick-menu-icon-line-height: calc(#{$slick-menu-icon-font-size} + 2px) !default;
$slick-menu-item-width-when-button: calc(100% - #{$slick-menu-close-btn-width}) !default;
-$slick-menu-icon-margin-right: 4px !default;
$slick-menu-icon-min-width: 16px !default;
$slick-menu-line-height: 24px !default;
$slick-menu-min-width: 140px !default;
diff --git a/packages/common/src/styles/slick-grid.scss b/packages/common/src/styles/slick-grid.scss
index e8e6d679a..307b2bdc8 100644
--- a/packages/common/src/styles/slick-grid.scss
+++ b/packages/common/src/styles/slick-grid.scss
@@ -322,6 +322,20 @@
display: flex;
}
+ .slick-rtl {
+ .slick-preheader-container,
+ .slick-header-container,
+ .slick-headerrow {
+ flex-direction: row-reverse;
+ }
+
+ .slick-resizable-handle {
+ right: auto;
+ inset-inline-end: auto;
+ left: -5px;
+ }
+ }
+
.slick-pane-top {
box-sizing: border-box;
border-top: var(--slick-pane-top-border-top, v.$slick-pane-top-border-top);
@@ -410,6 +424,10 @@
border-top: 0px !important;
border-bottom: 0px !important;
float: left;
+
+ .slick-rtl & {
+ float: right;
+ }
}
.slick-header-column {
@@ -610,7 +628,7 @@
top: var(--slick-icon-tree-load-fail-sup-top, v.$slick-icon-tree-load-fail-sup-top);
left: var(--slick-icon-tree-load-fail-sup-left, v.$slick-icon-tree-load-fail-sup-left);
font-size: var(--slick-icon-tree-load-fail-sup-font-size, v.$slick-icon-tree-load-fail-sup-font-size);
- right: var(--slick-icon-tree-load-fail-sup-right, v.$slick-icon-tree-load-fail-sup-right);
+ inset-inline-end: var(--slick-icon-tree-load-fail-sup-right, v.$slick-icon-tree-load-fail-sup-right);
color: var(--slick-icon-tree-load-fail-sup-color, v.$slick-icon-tree-load-fail-sup-color);
@include svg.generateSvgStyle('slick-icon-load-fail-sup-svg', v.$slick-icon-tree-load-fail-sup-svg-path);
}
@@ -679,6 +697,7 @@
}
}
.slick-header-columns {
+ display: flex;
background: var(--slick-grid-header-background, v.$slick-grid-header-background);
background-color: var(--slick-header-background-color, v.$slick-header-background-color);
@@ -750,7 +769,7 @@
width: 1em;
left: auto;
font-size: var(--slick-icon-sort-font-size, v.$slick-icon-sort-font-size);
- right: var(--slick-icon-sort-position-right, v.$slick-icon-sort-position-right);
+ inset-inline-end: var(--slick-icon-sort-position-right, v.$slick-icon-sort-position-right);
top: var(--slick-icon-sort-position-top, v.$slick-icon-sort-position-top);
}
.slick-sort-indicator-numbered {
@@ -758,7 +777,7 @@
font-size: var(--slick-sort-indicator-number-font-size, v.$slick-sort-indicator-number-font-size);
width: var(--slick-sort-indicator-number-width, v.$slick-sort-indicator-number-width);
left: var(--slick-sort-indicator-number-left, v.$slick-sort-indicator-number-left);
- right: var(--slick-sort-indicator-number-right, v.$slick-sort-indicator-number-right);
+ inset-inline-end: var(--slick-sort-indicator-number-right, v.$slick-sort-indicator-number-right);
top: var(--slick-sort-indicator-number-top, v.$slick-sort-indicator-number-top);
}
@@ -789,7 +808,7 @@
top: 0;
height: 100%;
width: 6px;
- right: 0;
+ inset-inline-end: 0;
z-index: 4;
&:hover {
@@ -800,7 +819,7 @@
border-top: var(--slick-header-resizable-hover-border-top, v.$slick-header-resizable-hover-border-top);
border-radius: var(--slick-header-resizable-hover-border-radius, v.$slick-header-resizable-hover-border-radius);
width: var(--slick-header-resizable-hover-width, v.$slick-header-resizable-hover-width);
- right: var(--slick-header-resizable-hover-right, v.$slick-header-resizable-hover-right);
+ inset-inline-end: var(--slick-header-resizable-hover-right, v.$slick-header-resizable-hover-right);
height: var(--slick-header-resizable-hover-height, v.$slick-header-resizable-hover-height);
top: var(--slick-header-resizable-hover-top, v.$slick-header-resizable-hover-top);
opacity: var(--slick-header-resizable-hover-opacity, v.$slick-header-resizable-hover-opacity);
diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss
index 071b4303d..46988c226 100644
--- a/packages/common/src/styles/slick-plugins.scss
+++ b/packages/common/src/styles/slick-plugins.scss
@@ -41,7 +41,6 @@ li.hidden {
}
.close {
- float: right;
position: absolute;
color: var(--slick-column-picker-close-btn-color, v.$slick-column-picker-close-btn-color);
cursor: var(--slick-column-picker-close-btn-cursor, v.$slick-column-picker-close-btn-cursor);
@@ -54,7 +53,7 @@ li.hidden {
font-size: var(--slick-column-picker-close-btn-font-size, v.$slick-column-picker-close-btn-font-size);
background-color: var(--slick-column-picker-close-btn-bg-color, v.$slick-column-picker-close-btn-bg-color);
border: var(--slick-column-picker-close-btn-border, v.$slick-column-picker-close-btn-border);
- right: var(--slick-column-picker-close-btn-position-right, v.$slick-column-picker-close-btn-position-right);
+ inset-inline-end: var(--slick-column-picker-close-btn-position-right, v.$slick-column-picker-close-btn-position-right);
top: var(--slick-column-picker-close-btn-position-top, v.$slick-column-picker-close-btn-position-top);
&:hover {
@@ -132,6 +131,7 @@ li.hidden {
height: 100%;
width: 100%;
margin-bottom: 0px;
+ gap: var(--slick-column-picker-label-gap, v.$slick-column-picker-label-gap);
}
}
@@ -198,7 +198,6 @@ li.hidden {
display: inline-flex;
align-items: center;
flex-grow: 1;
- padding-left: var(--slick-column-picker-label-text-padding-left, v.$slick-column-picker-label-text-padding-left);
}
}
}
@@ -245,7 +244,7 @@ li.hidden {
border: 0;
cursor: pointer;
position: absolute;
- right: 0;
+ inset-inline-end: 0;
z-index: 2;
color: var(--slick-grid-menu-icon-btn-color, v.$slick-grid-menu-icon-btn-color);
padding: var(--slick-grid-menu-button-padding, v.$slick-grid-menu-button-padding);
@@ -305,7 +304,6 @@ li.hidden {
.close {
cursor: pointer;
- float: right;
background-color: var(--slick-menu-close-btn-bg-color, v.$slick-menu-close-btn-bg-color);
border: var(--slick-menu-close-btn-border, v.$slick-menu-close-btn-border);
color: var(--slick-menu-close-btn-color, v.$slick-menu-close-btn-color);
@@ -335,6 +333,7 @@ li.hidden {
display: flex;
align-items: center;
margin: 0;
+ gap: var(--slick-menu-item-gap, v.$slick-menu-item-gap);
outline: none;
border: var(--slick-menu-item-border, v.$slick-menu-item-border);
border-radius: var(--slick-menu-item-border-radius, v.$slick-menu-item-border-radius);
@@ -379,7 +378,6 @@ li.hidden {
background-repeat: no-repeat;
display: inline-block;
line-height: var(--slick-menu-icon-line-height, v.$slick-menu-icon-line-height);
- margin-right: var(--slick-menu-icon-margin-right, v.$slick-menu-icon-margin-right);
vertical-align: middle;
min-width: var(--slick-menu-icon-min-width, v.$slick-menu-icon-min-width);
}
@@ -483,6 +481,11 @@ li.hidden {
float: left;
margin-bottom: 100px;
}
+.slick-column-name {
+ .slick-rtl & {
+ float: right;
+ }
+}
.slick-header-button {
/**
@@ -529,7 +532,7 @@ li.hidden {
// The next few items are already defined in the slick-headermenu file and it should stay that way, *unless* you also replace the button image included there.
bottom: 0;
top: 0;
- right: var(--slick-header-menu-button-margin-right, v.$slick-header-menu-button-margin-right);
+ inset-inline-end: var(--slick-header-menu-button-margin-right, v.$slick-header-menu-button-margin-right);
height: var(--slick-header-menu-button-icon-size, v.$slick-header-menu-button-icon-size);
width: var(--slick-header-menu-button-icon-size, v.$slick-header-menu-button-icon-size);
@@ -584,8 +587,6 @@ li.hidden {
.slick-column-name,
.slick-headerrow-column.checkbox-header,
.slick-cell-checkboxsel {
- text-align: center;
-
label {
line-height: var(--slick-checkbox-icon-container-line-height, v.$slick-checkbox-icon-container-line-height);
}
@@ -647,6 +648,8 @@ li.hidden {
}
}
+.slick-headerrow-column.checkbox-header,
+.slick-cell-checkboxsel,
.slick-header-column.header-checkbox-selectall .slick-column-name {
text-align: center;
margin-right: 0;
@@ -961,7 +964,7 @@ li.hidden {
padding: var(--slick-draggable-group-toggle-all-padding, v.$slick-draggable-group-toggle-all-padding);
position: var(--draggable-group-toggle-all-position, v.$slick-draggable-group-toggle-all-position);
top: var(--slick-draggable-group-toggle-all-top, v.$slick-draggable-group-toggle-all-top);
- right: var(--slick-draggable-group-toggle-all-right, v.$slick-draggable-group-toggle-all-right);
+ inset-inline-end: var(--slick-draggable-group-toggle-all-right, v.$slick-draggable-group-toggle-all-right);
.slick-group-toggle-all-icon {
cursor: pointer;
@@ -1294,5 +1297,5 @@ li.hidden {
background: var(--slick-drag-selection-handle-color, v.$slick-drag-selection-handle-color);
position: absolute;
bottom: 0;
- right: 0;
+ inset-inline-end: 0;
}
diff --git a/test/cypress/e2e/example46.cy.ts b/test/cypress/e2e/example46.cy.ts
new file mode 100644
index 000000000..6b13fec59
--- /dev/null
+++ b/test/cypress/e2e/example46.cy.ts
@@ -0,0 +1,99 @@
+describe('Example 46 - RTL (Right-to-Left)', () => {
+ const titles = ['ID', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
+
+ beforeEach(() => {
+ cy.setCookie('serve-mode', 'cypress');
+ cy.visit(`${Cypress.config('baseUrl')}/example46`);
+ });
+
+ describe('Basic Rendering', () => {
+ it('should display Example title', () => {
+ cy.get('h3').should('contain', 'Example 46 - RTL (Right-to-Left)');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('.grid46')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Configuration', () => {
+ it('should have RTL class applied to grid container', () => {
+ cy.get('.grid46')
+ .first()
+ .then(($grid) => {
+ const target = $grid.hasClass('slickgrid-container') ? $grid : $grid.find('.slickgrid-container');
+ cy.wrap(target).should('have.class', 'slick-rtl');
+ });
+ });
+
+ it('should have proper RTL cell content alignment', () => {
+ cy.get('.grid46 .slick-cell:first').should('have.css', 'direction', 'rtl');
+ });
+ });
+
+ describe('UI Interactions', () => {
+ it('should have resize handle on the left side', () => {
+ cy.get('.grid46 .slick-header-column:first .slick-resizable-handle').should('exist').and('have.css', 'left', '0px');
+ });
+
+ it('should maintain RTL column order after resize', () => {
+ cy.get('.grid46 .slick-header-column:first .slick-resizable-handle')
+ .trigger('mousedown', { which: 1 })
+ .then(() => {
+ cy.get('body').trigger('mousemove', { clientX: 260, clientY: 0 });
+ cy.get('body').trigger('mouseup');
+ });
+
+ cy.get('.grid46')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ describe('Scrolling Behavior', () => {
+ it('should have horizontal scroll enabled', () => {
+ cy.get('.grid46 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(viewport.scrollWidth).to.be.greaterThan(viewport.clientWidth);
+ });
+ });
+
+ it('should update visible header columns when scrolling', () => {
+ cy.get('.grid46 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = maxScroll;
+ if (viewport.scrollLeft === 0) {
+ viewport.scrollLeft = -maxScroll;
+ }
+ });
+
+ cy.wait(150);
+
+ cy.get('.grid46 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ expect(Math.abs(viewport.scrollLeft)).to.be.greaterThan(0);
+ });
+ });
+ });
+
+ describe('Edge Cases & Stability', () => {
+ it('should handle max horizontal scroll in RTL mode', () => {
+ cy.get('.grid46 .slick-viewport').then(($viewport) => {
+ const viewport = $viewport[0] as HTMLElement;
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = maxScroll;
+ if (viewport.scrollLeft === 0) {
+ viewport.scrollLeft = -maxScroll;
+ }
+ });
+
+ cy.wait(150);
+ cy.get('.grid46 .slick-header-column:visible').last().should('exist');
+ });
+ });
+});