Repository: exadel-inc/esl
Pull request #1499 (2023-02-23 00:22:57)

refactor(esl-utils): `RTLUtils` now deprecated and recreated in tree-shakable way


Patch:
diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts
index b76b6ad88..30d11b3fc 100644
--- a/src/modules/esl-popup/core/esl-popup.ts
+++ b/src/modules/esl-popup/core/esl-popup.ts
@@ -5,7 +5,7 @@ import {ESLTraversingQuery} from '../../esl-traversing-query/core';
 import {afterNextRender, rafDecorator} from '../../esl-utils/async/raf';
 import {ESLToggleable} from '../../esl-toggleable/core';
 import {Rect} from '../../esl-utils/dom/rect';
-import {RTLUtils} from '../../esl-utils/dom/rtl';
+import {isRTL} from '../../esl-utils/dom/rtl';
 import {getListScrollParents} from '../../esl-utils/dom/scroll';
 import {getWindowRect} from '../../esl-utils/dom/window';
 import {parseNumber} from '../../esl-utils/misc/format';
@@ -148,7 +148,7 @@ export class ESLPopup extends ESLToggleable {
   @memoize()
   protected get _offsetArrowRatio(): number {
     const ratio = parsePercent(this.offsetArrow, DEFAULT_OFFSET_ARROW) / 100;
-    return RTLUtils.isRtl(this) ? 1 - ratio : ratio;
+    return isRTL(this) ? 1 - ratio : ratio;
   }
 
   /** Moves popup into document.body */
diff --git a/src/modules/esl-scrollbar/core/esl-scrollbar.ts b/src/modules/esl-scrollbar/core/esl-scrollbar.ts
index 6399274df..c1681f527 100644
--- a/src/modules/esl-scrollbar/core/esl-scrollbar.ts
+++ b/src/modules/esl-scrollbar/core/esl-scrollbar.ts
@@ -5,7 +5,7 @@ import {rafDecorator} from '../../esl-utils/async/raf';
 import {isMouseEvent, isTouchEvent, getTouchPoint, getOffsetPoint} from '../../esl-utils/dom/events';
 import {isRelativeNode} from '../../esl-utils/dom/traversing';
 import {ESLTraversingQuery} from '../../esl-traversing-query/core';
-import {RTLUtils} from '../../esl-utils/dom/rtl';
+import {isRTL, RTLScroll, normalizeScrollLeft} from '../../esl-utils/dom/rtl';
 
 /**
  * ESLScrollbar is a reusable web component that replaces the browser's default scrollbar with
@@ -170,7 +170,7 @@ export class ESLScrollbar extends ESLBaseElement {
   /** Relative position value (between 0.0 and 1.0) */
   public get position(): number {
     if (!this.$target) return 0;
-    const scrollOffset = this.horizontal ? RTLUtils.normalizeScrollLeft(this.$target) : this.$target.scrollTop;
+    const scrollOffset = this.horizontal ? normalizeScrollLeft(this.$target) : this.$target.scrollTop;
     return this.scrollableSize ? (scrollOffset / this.scrollableSize) : 0;
   }
 
@@ -182,8 +182,8 @@ export class ESLScrollbar extends ESLBaseElement {
   /** Normalizes position value (between 0.0 and 1.0) */
   protected normalizePosition(position: number): number {
     const relativePosition = Math.min(1, Math.max(0, position));
-    if (!RTLUtils.isRtl(this.$target) || !this.horizontal) return relativePosition;
-    return RTLUtils.scrollType === 'negative' ? (relativePosition - 1) : (1 - relativePosition);
+    if (!isRTL(this.$target) || !this.horizontal) return relativePosition;
+    return RTLScroll.type === 'negative' ? (relativePosition - 1) : (1 - relativePosition);
   }
 
   /** Scrolls target element to passed position */
diff --git a/src/modules/esl-tab/core/esl-tabs.ts b/src/modules/esl-tab/core/esl-tabs.ts
index fcbd7573e..f986fdf54 100644
--- a/src/modules/esl-tab/core/esl-tabs.ts
+++ b/src/modules/esl-tab/core/esl-tabs.ts
@@ -2,7 +2,7 @@ import {ExportNs} from '../../esl-utils/environment/export-ns';
 import {ESLBaseElement} from '../../esl-base-element/core';
 import {rafDecorator} from '../../esl-utils/async/raf';
 import {memoize, attr, listen, decorate} from '../../esl-utils/decorators';
-import {RTLUtils} from '../../esl-utils/dom/rtl';
+import {isRTL, RTLScroll} from '../../esl-utils/dom/rtl';
 import {debounce} from '../../esl-utils/async/debounce';
 import {ESLMediaRuleList} from '../../esl-media-query/core/esl-media-rule-list';
 import {ESLTab} from './esl-tab';
@@ -99,7 +99,7 @@ export class ESLTabs extends ESLBaseElement {
     const $scrollableTarget = this.$scrollableTarget;
     if (!$scrollableTarget) return;
     let left = $scrollableTarget.offsetWidth;
-    left = RTLUtils.isRtl(this) && RTLUtils.scrollType !== 'reverse' ? -left : left;
+    left = isRTL(this) && RTLScroll.type !== 'reverse' ? -left : left;
     left = direction === 'left' ? -left : left;
 
     $scrollableTarget.scrollBy({left, behavior});
@@ -125,7 +125,7 @@ export class ESLTabs extends ESLBaseElement {
 
   /** Get scroll offset position from the selected item rectangle */
   protected calcScrollOffset(itemRect: DOMRect, areaRect: DOMRect): number | undefined {
-    const isReversedRTL = RTLUtils.isRtl(this) && RTLUtils.scrollType === 'reverse';
+    const isReversedRTL = isRTL(this) && RTLScroll.type === 'reverse';
 
     if (this.currentScrollableType === 'center') {
       const shift = itemRect.left + itemRect.width / 2 - (areaRect.left + areaRect.width / 2);
@@ -145,7 +145,7 @@ export class ESLTabs extends ESLBaseElement {
     const $scrollableTarget = this.$scrollableTarget;
     if (!$scrollableTarget) return;
 
-    const swapSides = RTLUtils.isRtl(this) && RTLUtils.scrollType === 'default';
+    const swapSides = isRTL(this) && RTLScroll.type === 'default';
     const scrollStart = Math.abs($scrollableTarget.scrollLeft) > 1;
     const scrollEnd = Math.abs($scrollableTarget.scrollLeft) + $scrollableTarget.clientWidth + 1 < $scrollableTarget.scrollWidth;
 
diff --git a/src/modules/esl-utils/dom/rtl.ts b/src/modules/esl-utils/dom/rtl.ts
index b2a81b881..ef011694c 100644
--- a/src/modules/esl-utils/dom/rtl.ts
+++ b/src/modules/esl-utils/dom/rtl.ts
@@ -1,45 +1,12 @@
-import {memoize} from '../decorators/memoize';
-
+// TODO: Revisit using https://caniuse.com/mdn-api_element_scrollleft in 5.0.0
+/** RTL scroll browser behaviours */
 export type ScrollType = 'default' | 'negative' | 'reverse';
 
-// TODO: functional
-export abstract class RTLUtils {
-  /** Check if the element in a RTL direction context */
-  static isRtl(el?: HTMLElement | null): boolean {
-    return getComputedStyle(el || document.body).direction === 'rtl';
-  }
-
-  /**
-   * @returns RTL scroll type (lazy, memoized)
-   */
-  @memoize()
-  static get scrollType(): ScrollType {
-    let scrollType: ScrollType = 'default';
-    const el = createDummyEl();
-    document.body.appendChild(el);
-    if (el.scrollLeft <= 0) {
-      el.scrollLeft = 2;
-      scrollType = el.scrollLeft < 2 ? 'negative' : 'reverse';
-    }
-    document.body.removeChild(el);
-    return scrollType;
-  }
-
-  static normalizeScrollLeft(el: HTMLElement, value: number | null = null, isRtl: boolean = RTLUtils.isRtl(el)): number {
-    value = (value === null) ? el.scrollLeft : value;
-    switch (isRtl ? RTLUtils.scrollType : '') {
-      case 'negative':
-        return el.scrollWidth - el.clientWidth + value;
-      case 'reverse':
-        return el.scrollWidth - el.clientWidth - value;
-      default:
-        return value;
-    }
-  }
-}
+/** Checks if the element in a RTL direction context */
+export const isRTL = (el?: HTMLElement | null): boolean => getComputedStyle(el || document.body).direction === 'rtl';
 
 /** Creates the dummy test element with a horizontal scroll presented */
-function createDummyEl(): HTMLElement {
+const createDummyScroll = (): HTMLElement => {
   const el = document.createElement('div');
   el.appendChild(document.createTextNode('ESL!'));
   el.dir = 'rtl';
@@ -52,4 +19,52 @@ function createDummyEl(): HTMLElement {
     overflow: 'scroll'
   });
   return el;
+};
+
+export const testRTLScrollType = (): ScrollType => {
+  let scrollType: ScrollType = 'default';
+  const el = createDummyScroll();
+  document.body.appendChild(el);
+  if (el.scrollLeft <= 0) {
+    el.scrollLeft = 2;
+    scrollType = el.scrollLeft < 2 ? 'negative' : 'reverse';
+  }
+  document.body.removeChild(el);
+  return scrollType;
+};
+
+let type: ScrollType | null = null;
+
+export const RTLScroll = {
+  /** @returns RTL scroll type (lazy, memoized) */
+  get type(): ScrollType {
+    if (typeof type === 'string') return type;
+    return type = testRTLScrollType();
+  }
+};
+
+export const normalizeScrollLeft = (el: HTMLElement, value: number | null = null, isRtl: boolean = isRTL(el)): number => {
+  value = (value === null) ? el.scrollLeft : value;
+  switch (isRtl ? RTLScroll.type : '') {
+    case 'negative':
+      return el.scrollWidth - el.clientWidth + value;
+    case 'reverse':
+      return el.scrollWidth - el.clientWidth - value;
+    default:
+      return value;
+  }
+};
+
+/** @deprecated use separate functions from the module */
+export abstract class RTLUtils {
+  /** @deprecated use {@link isRTL} instead */
+  static readonly isRtl = isRTL;
+
+  /** @deprecated use {@link normalizeScrollLeft} instead */
+  static readonly normalizeScrollLeft = normalizeScrollLeft;
+
+  /** @deprecated use {@link RTLScroll}.type instead */
+  static get scrollType(): ScrollType {
+    return RTLScroll.type;
+  }
 }


Test patch:
diff --git a/src/modules/esl-utils/dom/test/rtl.test.ts b/src/modules/esl-utils/dom/test/rtl.test.ts
index fa91faebf..58e6a863f 100644
--- a/src/modules/esl-utils/dom/test/rtl.test.ts
+++ b/src/modules/esl-utils/dom/test/rtl.test.ts
@@ -1,4 +1,4 @@
-import {RTLUtils} from '../rtl';
+import {RTLScroll, isRTL, normalizeScrollLeft} from '../rtl';
 
 describe('RTLUtils', () => {
   describe('isRtl', () => {
@@ -7,8 +7,8 @@ describe('RTLUtils', () => {
       const el = document.createElement('div');
       root.appendChild(el);
 
-      expect(RTLUtils.isRtl(root)).toBe(false);
-      expect(RTLUtils.isRtl(el)).toBe(false);
+      expect(isRTL(root)).toBe(false);
+      expect(isRTL(el)).toBe(false);
     });
     test('rtl', () => {
       const root = document.createElement('div');
@@ -18,9 +18,9 @@ describe('RTLUtils', () => {
       const gcsSpy = jest.spyOn(window, 'getComputedStyle');
 
       gcsSpy.mockImplementationOnce(() => ({direction: 'rtl'} as CSSStyleDeclaration));
-      expect(RTLUtils.isRtl(root)).toBe(true);
+      expect(isRTL(root)).toBe(true);
       gcsSpy.mockImplementationOnce(() => ({direction: 'rtl'} as CSSStyleDeclaration));
-      expect(RTLUtils.isRtl(el)).toBe(true);
+      expect(isRTL(el)).toBe(true);
     });
 
     test('normalizeScrollLeft', () => {
@@ -29,27 +29,19 @@ describe('RTLUtils', () => {
       jest.spyOn(el, 'scrollWidth', 'get').mockImplementation(() => 1000);
       jest.spyOn(el, 'clientWidth', 'get').mockImplementation(() => 100);
       jest.spyOn(el, 'scrollLeft', 'get').mockImplementation(() => 200);
-      jest.spyOn(RTLUtils, 'scrollType', 'get').mockImplementation(() => 'negative');
+      jest.spyOn(RTLScroll, 'type', 'get').mockImplementation(() => 'negative');
 
-      expect(RTLUtils.normalizeScrollLeft(el)).toBe(200);
-      expect(RTLUtils.normalizeScrollLeft(el, 50)).toBe(50);
-      expect(RTLUtils.normalizeScrollLeft(el, 50, false)).toBe(50);
-      expect(RTLUtils.normalizeScrollLeft(el, null, false)).toBe(200);
-      expect(RTLUtils.normalizeScrollLeft(el, 50, true)).toBe(950);
-      expect(RTLUtils.normalizeScrollLeft(el, null, true)).toBe(1100);
+      expect(normalizeScrollLeft(el)).toBe(200);
+      expect(normalizeScrollLeft(el, 50)).toBe(50);
+      expect(normalizeScrollLeft(el, 50, false)).toBe(50);
+      expect(normalizeScrollLeft(el, null, false)).toBe(200);
+      expect(normalizeScrollLeft(el, 50, true)).toBe(950);
+      expect(normalizeScrollLeft(el, null, true)).toBe(1100);
 
-      jest.spyOn(RTLUtils, 'scrollType', 'get').mockImplementation(() => 'reverse');
+      jest.spyOn(RTLScroll, 'type', 'get').mockImplementation(() => 'reverse');
 
-      expect(RTLUtils.normalizeScrollLeft(el, 50, true)).toBe(850);
-      expect(RTLUtils.normalizeScrollLeft(el, null, true)).toBe(700);
+      expect(normalizeScrollLeft(el, 50, true)).toBe(850);
+      expect(normalizeScrollLeft(el, null, true)).toBe(700);
     });
   });
-
-  test('scrollType', () => {
-    // Should be negative in the JSDOM env
-    expect(RTLUtils.scrollType).toBe('reverse');
-    // Double test for cache
-    expect(RTLUtils.scrollType).toBe('reverse');
-    // TODO: emulation and tests for other cases
-  });
 });