diff --git a/cypress/e2e/example-rtl.cy.ts b/cypress/e2e/example-rtl.cy.ts
new file mode 100644
index 00000000..f5b45beb
--- /dev/null
+++ b/cypress/e2e/example-rtl.cy.ts
@@ -0,0 +1,203 @@
+describe('Example - RTL (Right-to-Left) Support', () => {
+ const titles = [
+ 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven',
+ 'Priority', 'Status', 'Assignee', 'Department', 'Project', 'Completed'
+ ];
+
+ beforeEach(() => {
+ cy.visit(`${Cypress.config('baseUrl')}/examples/example1-simple-rtl.html`);
+ });
+
+ // Section 1: Basic Rendering
+
+ describe('Basic Rendering', () => {
+ it('should display Example title', () => {
+ cy.get('h2').should('contain', 'Simple Grid (RTL)');
+ });
+
+ it('should have exact Column Titles in the grid', () => {
+ cy.get('#myGrid')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+
+ it('should render columns in right-to-left order', () => {
+ cy.get('#myGrid')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ // Section 2: Configuration & Setup
+
+ describe('Configuration', () => {
+ it('should have RTL class applied to grid container', () => {
+ cy.get('#myGrid')
+ .should('have.class', 'slick-rtl');
+ });
+
+ it('should have RTL option enabled in grid options', () => {
+ cy.window().then((win) => {
+ const grid = (win as any).grid;
+ if (grid && grid.getOptions) {
+ const options = grid.getOptions();
+ expect(options.rtl).to.be.true;
+ }
+ });
+ });
+
+ it('should have proper RTL cell content alignment', () => {
+ cy.get('#myGrid')
+ .find('.slick-cell:first')
+ .should('have.css', 'direction', 'rtl');
+ });
+ });
+
+ // Section 3: UI Interactions
+
+ describe('UI Interactions', () => {
+ it('should have resize handle on the left side', () => {
+ cy.get('#myGrid')
+ .find('.slick-header-column:first .slick-resizable-handle')
+ .should('exist')
+ .and('have.css', 'left', '0px');
+ });
+
+ it('should maintain RTL behavior after column resize', () => {
+ // Resize first column
+ cy.get('#myGrid')
+ .find('.slick-header-column:first .slick-resizable-handle')
+ .trigger('mousedown', { which: 1 })
+ .trigger('mousemove', { clientX: 30 })
+ .trigger('mouseup');
+
+ // Verify columns still in correct RTL order
+ cy.get('#myGrid')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+ });
+
+ // Section 4: Scrolling Behavior
+
+ describe('Scrolling Behavior', () => {
+ it('should have horizontal scroll enabled', () => {
+ cy.get('.slick-viewport')
+ .should('have.prop', 'scrollWidth')
+ .then((scrollWidth) => {
+ cy.get('.slick-viewport')
+ .invoke('width')
+ .should((viewportWidth) => {
+ // @ts-ignore - scrollWidth and viewportWidth are numbers
+ expect(scrollWidth).to.be.greaterThan(viewportWidth);
+ });
+ });
+ });
+
+ it('should scroll horizontally in RTL mode', () => {
+ cy.get('.slick-viewport')
+ .then(($viewport) => {
+ const viewport = $viewport[0];
+ viewport.scrollLeft = -200;
+ cy.wait(100);
+ expect(viewport.scrollLeft).to.be.lessThan(0);
+ });
+ });
+
+ it('should update visible range when scrolling in RTL', () => {
+ let initialFirstColumn = '';
+ cy.get('.slick-header-column:visible')
+ .first()
+ .invoke('text')
+ .then((text) => {
+ initialFirstColumn = text;
+ });
+
+ cy.get('.slick-viewport')
+ .then(($viewport) => {
+ const viewport = $viewport[0];
+ viewport.scrollLeft = -300;
+ cy.wait(150);
+ });
+
+ cy.get('.slick-header-column:visible')
+ .first()
+ .invoke('text')
+ .should((newText) => {
+ expect(newText).not.to.equal(initialFirstColumn);
+ });
+ });
+
+ it('should calculate correct visible range in RTL mode', () => {
+ cy.window().then((win) => {
+ const grid = (win as any).grid;
+ if (grid && grid.getVisibleRange) {
+ const viewport = grid._viewport;
+ const originalScrollLeft = viewport.scrollLeft;
+ viewport.scrollLeft = -200;
+ const range = grid.getVisibleRange();
+ expect(range.leftPx).to.be.a('number');
+ expect(range.rightPx).to.be.a('number');
+ expect(range.rightPx).to.be.greaterThan(range.leftPx);
+ viewport.scrollLeft = originalScrollLeft;
+ }
+ });
+ });
+ });
+
+ // Section 5: Edge Cases & Stability
+
+ describe('Edge Cases & Stability', () => {
+ it('should handle max scroll in RTL mode', () => {
+ cy.get('.slick-viewport')
+ .then(($viewport) => {
+ const viewport = $viewport[0];
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = -maxScroll;
+ cy.wait(150);
+ cy.get('.slick-header-column:visible')
+ .last()
+ .should('exist');
+ });
+ });
+
+ it('should maintain scroll position after column updates', () => {
+ let currentScrollLeft = 0;
+ cy.get('.slick-viewport')
+ .then(($viewport) => {
+ const viewport = $viewport[0];
+ viewport.scrollLeft = -300;
+ currentScrollLeft = viewport.scrollLeft;
+ cy.wait(100);
+ cy.window().then((win) => {
+ const grid = (win as any).grid;
+ if (grid && grid.render) {
+ grid.render();
+ }
+ });
+ cy.wait(100);
+ expect(viewport.scrollLeft).to.equal(currentScrollLeft);
+ });
+ });
+
+ it('should scroll to the end and display last columns', () => {
+ cy.get('.slick-viewport')
+ .then(($viewport) => {
+ const viewport = $viewport[0];
+ const maxScroll = viewport.scrollWidth - viewport.clientWidth;
+ viewport.scrollLeft = -maxScroll;
+ cy.wait(300);
+ });
+
+ cy.get('.slick-header-column:visible')
+ .first()
+ .invoke('text')
+ .then((text) => {
+ expect(text).not.to.equal('Title');
+ });
+ });
+ });
+});
diff --git a/examples/example1-simple-rtl.html b/examples/example1-simple-rtl.html
new file mode 100644
index 00000000..753a50e0
--- /dev/null
+++ b/examples/example1-simple-rtl.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+ SlickGrid example 1: Basic grid (RTL)
+
+
+
+
+
+ Example 1 Simple Grid (RTL) - ESM
+
+
+ |
+
+ |
+
+
+
+ ⌂
+ Demonstrates:
+
+
+
+ - basic grid with minimal configuration
+ - RTL (right-to-left) support enabled
+
+ View Source:
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/index.html b/examples/index.html
index 0dc6bdea..3bfd6744 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -65,6 +65,7 @@ Basic Use
Handling events and context menu
Highlighting and flashing cells
Adding some formatting
+ Basic use with RTL support
Editing
diff --git a/src/models/gridOption.interface.ts b/src/models/gridOption.interface.ts
index 02fb42db..08720884 100644
--- a/src/models/gridOption.interface.ts
+++ b/src/models/gridOption.interface.ts
@@ -350,6 +350,9 @@ export interface GridOption {
/** Defaults to 400, duration to show the row highlight (e.g. after CRUD executions) */
rowHighlightDuration?: number;
+ /** Defaults to false, sets the grid direction to RTL (Right-to-Left) for proper rendering of RTL languages */
+ rtl?: boolean;
+
/**
* Defaults to "top", what CSS style to we want to use to render each row top offset (we can use "top" or "transform").
* For example, with a default `rowHeight: 22`, the 2nd row will have a `top` offset of 44px and by default have a CSS style of `top: 44px`.
diff --git a/src/slick.grid.ts b/src/slick.grid.ts
index 186a0ef6..6082f644 100644
--- a/src/slick.grid.ts
+++ b/src/slick.grid.ts
@@ -334,7 +334,8 @@ export class SlickGrid = Column, O e
logSanitizedHtml: false, // log to console when sanitised - recommend true for testing of dev and production
mixinDefaults: true,
shadowRoot: undefined,
- colAutosizeTreatAsLockedBelowWidth: 100
+ colAutosizeTreatAsLockedBelowWidth: 100,
+ rtl: false
};
protected _columnDefaults = {
@@ -758,8 +759,8 @@ export class SlickGrid = Column, O e
this._headerScroller.push(this._headerScrollerR);
// Append the columnn containers to the headers
- this._headerL = Utils.createDomElement('div', { className: 'slick-header-columns slick-header-columns-left', role: 'row', style: { left: '-1000px' } }, this._headerScrollerL);
- this._headerR = Utils.createDomElement('div', { className: 'slick-header-columns slick-header-columns-right', role: 'row', style: { left: '-1000px' } }, this._headerScrollerR);
+ this._headerL = Utils.createDomElement('div', { className: 'slick-header-columns slick-header-columns-left', role: 'row', style: { [this.dirSide]: '-1000px' } }, this._headerScrollerL);
+ this._headerR = Utils.createDomElement('div', { className: 'slick-header-columns slick-header-columns-right', role: 'row', style: { [this.dirSide]: '-1000px' } }, this._headerScrollerR);
// Cache the header columns
this._headers = [this._headerL, this._headerR];
@@ -867,6 +868,8 @@ export class SlickGrid = Column, O e
if (!this._options.explicitInitialization) {
this.finishInitialization();
}
+
+ this.applyRTL(this._options.rtl ?? false);
}
/**
@@ -2173,14 +2176,25 @@ export class SlickGrid = Column, O e
if (stretchLeewayOnLeft === null) {
stretchLeewayOnLeft = 100000;
}
- maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
- minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
+
+ if (this._options.rtl) {
+ maxPageX = pageX + Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
+ minPageX = pageX - Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
+ } else {
+ maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
+ minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
+ }
},
onResize: (e, resizeElms) => {
const targetEvent = (e as TouchEvent).touches ? (e as TouchEvent).changedTouches[0] : e;
this.columnResizeDragging = true;
let actualMinWidth;
- const d = Math.min(maxPageX, Math.max(minPageX, (targetEvent as MouseEvent).pageX)) - pageX;
+ let d = Math.min(maxPageX, Math.max(minPageX, (targetEvent as MouseEvent).pageX)) - pageX;
+
+ if (this._options.rtl) {
+ d = -d;
+ }
+
let x;
let newCanvasWidthL = 0;
let newCanvasWidthR = 0;
@@ -3164,8 +3178,14 @@ export class SlickGrid = Column, O e
w = this.columns[i].width || 0;
rule = this.getColumnCssRules(i);
- rule.left.style.left = `${x}px`;
- rule.right.style.right = (((this._options.frozenColumn !== -1 && i > this._options.frozenColumn!) ? this.canvasWidthR : this.canvasWidthL) - x - w) + 'px';
+
+ if (this._options.rtl) {
+ rule.left.style.right = `${x}px`;
+ rule.right.style.left = (((this._options.frozenColumn !== -1 && i > this._options.frozenColumn!) ? this.canvasWidthR : this.canvasWidthL) - x - w) + 'px';
+ } else {
+ rule.left.style.left = `${x}px`;
+ 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
// column starts in a new viewport.
@@ -5074,8 +5094,8 @@ export class SlickGrid = Column, O e
const rowHeight = (this._options.rowHeight! - this.cellHeightDiff);
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; }`,
@@ -6245,11 +6265,22 @@ 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, scrollLeft represents the offset from the RIGHT edge.
+ // The viewport's left edge is: maxScroll (far left) - scrollLeft (offset from right)
+ 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
};
}
@@ -8319,6 +8350,39 @@ export class SlickGrid = Column, O e
return cleanHtml;
}
+ /**
+ * 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() {
+ return this._options.rtl ? 'right' : 'left';
+ }
+
+ /**
+ * Applies or removes RTL (Right-to-Left) support on the grid container.
+ *
+ * When enabled, this method:
+ * - Adds the `slick-rtl` CSS class for styling
+ * - Sets the `dir="rtl"` attribute for proper text direction
+ * When disabled, it removes both the class and attribute.
+ * This makes the grid self-contained, allowing RTL to work regardless of the page's direction setting.
+ *
+ * @param enabled - Whether RTL should be enabled
+ */
+ 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');
+ }
+ }
+
///////////////////////////////////////////////////////////////
// End Shared Utilities and Accessors
///////////////////////////////////////////////////////////////
diff --git a/src/styles/slick-alpine-theme.scss b/src/styles/slick-alpine-theme.scss
index 3427af04..87c438c3 100644
--- a/src/styles/slick-alpine-theme.scss
+++ b/src/styles/slick-alpine-theme.scss
@@ -866,3 +866,9 @@ button.slick-btn {
bottom: 0;
right: 0;
}
+
+/* RTL (Right-to-Left) Support */
+.slick-rtl .slick-resizable-handle {
+ right: auto;
+ left: 0;
+}
diff --git a/src/styles/slick.grid.scss b/src/styles/slick.grid.scss
index 39ccd548..decffbc4 100644
--- a/src/styles/slick.grid.scss
+++ b/src/styles/slick.grid.scss
@@ -273,3 +273,9 @@ classes should alter those!
outline: 0;
width: 100%;
}
+
+/* RTL (Right-to-Left) Support */
+.slick-rtl .slick-resizable-handle {
+ right: auto;
+ left: -5px;
+}