@ringcentral/spring-base 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hooks.js CHANGED
@@ -13,7 +13,6 @@ import 'lodash/isPlainObject';
13
13
  import debounce from 'lodash/debounce';
14
14
  import noop from 'lodash/noop';
15
15
  import throttle from 'lodash/throttle';
16
- import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
17
16
  import 'tailwind-merge';
18
17
 
19
18
  const useEventCallback = (func) => {
@@ -1479,31 +1478,369 @@ const useFontLoaded = (enabled = true) => {
1479
1478
  return loaded;
1480
1479
  };
1481
1480
 
1481
+ function createInitialState() {
1482
+ return {
1483
+ entries: [],
1484
+ savedBodyOverflow: '',
1485
+ savedBodyPaddingRight: null,
1486
+ docTouchStartHandler: null,
1487
+ docTouchMoveHandler: null,
1488
+ touchStartY: 0,
1489
+ touchStartX: 0,
1490
+ nonIOSLocked: false,
1491
+ nonIOSReservedScrollBarGap: false,
1492
+ };
1493
+ }
1494
+ function isIOS(win) {
1495
+ const { platform, maxTouchPoints, userAgent } = win.navigator;
1496
+ return (/iP(ad|hone|od)/.test(platform) ||
1497
+ (platform === 'MacIntel' && maxTouchPoints > 1) ||
1498
+ /iPad|iPhone|iPod/.test(userAgent));
1499
+ }
1500
+ function isScrollableElement(el, win) {
1501
+ const { overflow, overflowX, overflowY } = win.getComputedStyle(el);
1502
+ if (!/(auto|scroll|overlay)/.test(overflow + overflowX + overflowY))
1503
+ return false;
1504
+ return (el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1);
1505
+ }
1506
+ function isScrollBoundaryHit(scrollable, dx, dy) {
1507
+ if (Math.abs(dy) >= Math.abs(dx)) {
1508
+ if (dy > 0 && scrollable.scrollTop <= 0)
1509
+ return true;
1510
+ return (dy < 0 &&
1511
+ scrollable.scrollTop + scrollable.clientHeight >=
1512
+ scrollable.scrollHeight - 1);
1513
+ }
1514
+ if (dx > 0 && scrollable.scrollLeft <= 0)
1515
+ return true;
1516
+ return (dx < 0 &&
1517
+ scrollable.scrollLeft + scrollable.clientWidth >= scrollable.scrollWidth - 1);
1518
+ }
1519
+ function findNearestScrollableAncestorWithinBoundary(el, win, boundary) {
1520
+ const { body, documentElement } = win.document;
1521
+ let cur = el;
1522
+ while (cur && cur !== body && cur !== documentElement) {
1523
+ if (isScrollableElement(cur, win))
1524
+ return cur;
1525
+ if (cur === boundary)
1526
+ break;
1527
+ cur = cur.parentElement;
1528
+ }
1529
+ return null;
1530
+ }
1531
+ function getActiveEntries(state) {
1532
+ return state.entries.filter((entry) => entry.active);
1533
+ }
1534
+ function getBoundaryForEntry(topEntry) {
1535
+ return topEntry.kind === 'element' ? topEntry.element : null;
1536
+ }
1537
+ function getScrollableWithinBoundary(target, win, boundary) {
1538
+ if (boundary && !boundary.contains(target)) {
1539
+ return null;
1540
+ }
1541
+ return findNearestScrollableAncestorWithinBoundary(target, win, boundary);
1542
+ }
1543
+ function getEntryScopedScrollable(topEntry, target, win, { allowAnonymous = false } = {}) {
1544
+ const boundary = getBoundaryForEntry(topEntry);
1545
+ if (!boundary && !allowAnonymous) {
1546
+ return null;
1547
+ }
1548
+ return getScrollableWithinBoundary(target, win, boundary);
1549
+ }
1550
+ function getAllowTouchMoveScrollable(entry, target, win) {
1551
+ if (entry.allowTouchMove?.(target) !== true)
1552
+ return null;
1553
+ return getEntryScopedScrollable(entry, target, win, {
1554
+ allowAnonymous: true,
1555
+ });
1556
+ }
1557
+ function canEntryAllowInnerScroll(entry, target, win, dx, dy) {
1558
+ const allowTouchMoveScrollable = getAllowTouchMoveScrollable(entry, target, win);
1559
+ if (allowTouchMoveScrollable) {
1560
+ return !isScrollBoundaryHit(allowTouchMoveScrollable, dx, dy);
1561
+ }
1562
+ const boundaryScrollable = getEntryScopedScrollable(entry, target, win);
1563
+ if (boundaryScrollable) {
1564
+ return !isScrollBoundaryHit(boundaryScrollable, dx, dy);
1565
+ }
1566
+ return false;
1567
+ }
1568
+ function findEntryIndex(state, targetElement, entryKey) {
1569
+ if (entryKey) {
1570
+ return state.entries.findIndex((entry) => entry.key === entryKey);
1571
+ }
1572
+ if (targetElement) {
1573
+ for (let i = state.entries.length - 1; i >= 0; i--) {
1574
+ const entry = state.entries[i];
1575
+ if (entry.kind === 'element' && entry.element === targetElement) {
1576
+ return i;
1577
+ }
1578
+ }
1579
+ return -1;
1580
+ }
1581
+ for (let i = state.entries.length - 1; i >= 0; i--) {
1582
+ if (state.entries[i].kind === 'anonymous') {
1583
+ return i;
1584
+ }
1585
+ }
1586
+ return -1;
1587
+ }
1588
+ function ensureEntry(state, targetElement, options = {}) {
1589
+ const index = findEntryIndex(state, targetElement, options.entryKey);
1590
+ const kind = targetElement ? 'element' : 'anonymous';
1591
+ if (index >= 0) {
1592
+ const entry = state.entries[index];
1593
+ entry.kind = kind;
1594
+ entry.element = targetElement ?? null;
1595
+ entry.reserveScrollBarGap = Boolean(options.reserveScrollBarGap);
1596
+ entry.allowTouchMove = options.allowTouchMove;
1597
+ return entry;
1598
+ }
1599
+ const entry = {
1600
+ key: options.entryKey ?? Symbol('scroll-lock-entry'),
1601
+ kind,
1602
+ element: targetElement ?? null,
1603
+ active: true,
1604
+ reserveScrollBarGap: Boolean(options.reserveScrollBarGap),
1605
+ allowTouchMove: options.allowTouchMove,
1606
+ };
1607
+ state.entries.push(entry);
1608
+ return entry;
1609
+ }
1610
+ function shouldPreventTouchMove(e, state, win) {
1611
+ if (e.touches.length > 1)
1612
+ return false;
1613
+ if (e.ctrlKey)
1614
+ return false;
1615
+ if (!e.cancelable)
1616
+ return false;
1617
+ const touch = e.touches[0];
1618
+ if (!touch)
1619
+ return false;
1620
+ const dy = touch.clientY - state.touchStartY;
1621
+ const dx = touch.clientX - state.touchStartX;
1622
+ if (dy === 0 && dx === 0)
1623
+ return false;
1624
+ if (getActiveEntries(state).length === 0)
1625
+ return false;
1626
+ const target = e.target;
1627
+ if (!(target instanceof win.Element)) {
1628
+ return true;
1629
+ }
1630
+ for (let i = state.entries.length - 1; i >= 0; i--) {
1631
+ if (canEntryAllowInnerScroll(state.entries[i], target, win, dx, dy)) {
1632
+ return false;
1633
+ }
1634
+ }
1635
+ return true;
1636
+ }
1637
+ function lockIOS(state, win) {
1638
+ const doc = win.document;
1639
+ state.docTouchStartHandler = (e) => {
1640
+ if (e.touches.length > 0) {
1641
+ state.touchStartY = e.touches[0].clientY;
1642
+ state.touchStartX = e.touches[0].clientX;
1643
+ }
1644
+ };
1645
+ state.docTouchMoveHandler = (e) => {
1646
+ if (shouldPreventTouchMove(e, state, win)) {
1647
+ e.preventDefault();
1648
+ }
1649
+ };
1650
+ doc.addEventListener('touchstart', state.docTouchStartHandler, {
1651
+ passive: true,
1652
+ });
1653
+ doc.addEventListener('touchmove', state.docTouchMoveHandler, {
1654
+ passive: false,
1655
+ });
1656
+ }
1657
+ function unlockIOS(state, win) {
1658
+ const doc = win.document;
1659
+ if (state.docTouchStartHandler) {
1660
+ doc.removeEventListener('touchstart', state.docTouchStartHandler);
1661
+ state.docTouchStartHandler = null;
1662
+ }
1663
+ if (state.docTouchMoveHandler) {
1664
+ doc.removeEventListener('touchmove', state.docTouchMoveHandler);
1665
+ state.docTouchMoveHandler = null;
1666
+ }
1667
+ }
1668
+ function lockNonIOS(state, win, { reserveScrollBarGap = false }) {
1669
+ const body = win.document.body;
1670
+ const gap = win.innerWidth - win.document.documentElement.clientWidth;
1671
+ state.savedBodyOverflow = body.style.overflow;
1672
+ if (reserveScrollBarGap && gap > 0) {
1673
+ const computedPaddingRight = parseInt(win.getComputedStyle(body).paddingRight, 10) || 0;
1674
+ state.savedBodyPaddingRight = body.style.paddingRight;
1675
+ body.style.paddingRight = `${computedPaddingRight + gap}px`;
1676
+ }
1677
+ body.style.overflow = 'hidden';
1678
+ }
1679
+ function unlockNonIOS(state, win) {
1680
+ const body = win.document.body;
1681
+ body.style.overflow = state.savedBodyOverflow;
1682
+ if (state.savedBodyPaddingRight !== null) {
1683
+ if (state.savedBodyPaddingRight === '') {
1684
+ body.style.removeProperty('padding-right');
1685
+ }
1686
+ else {
1687
+ body.style.paddingRight = state.savedBodyPaddingRight;
1688
+ }
1689
+ state.savedBodyPaddingRight = null;
1690
+ }
1691
+ }
1692
+ function bodyLockDesired(state) {
1693
+ return getActiveEntries(state).length > 0;
1694
+ }
1695
+ function shouldReserveScrollBarGap(state) {
1696
+ return state.entries.some((entry) => entry.active && entry.reserveScrollBarGap);
1697
+ }
1698
+ function syncIOS(state, win) {
1699
+ const desired = state.entries.length > 0;
1700
+ const attached = state.docTouchMoveHandler !== null;
1701
+ if (desired === attached)
1702
+ return;
1703
+ if (desired) {
1704
+ lockIOS(state, win);
1705
+ }
1706
+ else {
1707
+ unlockIOS(state, win);
1708
+ }
1709
+ }
1710
+ function syncNonIOS(state, win) {
1711
+ const desired = bodyLockDesired(state);
1712
+ const reserveScrollBarGap = shouldReserveScrollBarGap(state);
1713
+ if (!desired) {
1714
+ if (!state.nonIOSLocked)
1715
+ return;
1716
+ unlockNonIOS(state, win);
1717
+ state.nonIOSLocked = false;
1718
+ state.nonIOSReservedScrollBarGap = false;
1719
+ return;
1720
+ }
1721
+ if (!state.nonIOSLocked) {
1722
+ lockNonIOS(state, win, {
1723
+ reserveScrollBarGap,
1724
+ });
1725
+ state.nonIOSLocked = true;
1726
+ state.nonIOSReservedScrollBarGap = reserveScrollBarGap;
1727
+ return;
1728
+ }
1729
+ if (reserveScrollBarGap === state.nonIOSReservedScrollBarGap)
1730
+ return;
1731
+ unlockNonIOS(state, win);
1732
+ lockNonIOS(state, win, { reserveScrollBarGap });
1733
+ state.nonIOSReservedScrollBarGap = reserveScrollBarGap;
1734
+ }
1735
+ function syncScrollLock(state, win) {
1736
+ if (isIOS(win)) {
1737
+ syncIOS(state, win);
1738
+ }
1739
+ else {
1740
+ syncNonIOS(state, win);
1741
+ }
1742
+ }
1743
+ function activateEntry(state, win, targetElement, options = {}) {
1744
+ const entry = ensureEntry(state, targetElement, options);
1745
+ entry.active = true;
1746
+ syncScrollLock(state, win);
1747
+ }
1748
+ function lock(state, win, targetElement, options = {}) {
1749
+ activateEntry(state, win, targetElement, options);
1750
+ }
1751
+ function unlock(state, win, targetElement, options = {}) {
1752
+ const index = findEntryIndex(state, targetElement, options.entryKey);
1753
+ if (index < 0)
1754
+ return;
1755
+ state.entries.splice(index, 1);
1756
+ syncScrollLock(state, win);
1757
+ }
1758
+ function pause(state, win, targetElement, options = {}) {
1759
+ const index = findEntryIndex(state, targetElement, options.entryKey);
1760
+ if (index < 0 || state.entries[index].active === false)
1761
+ return;
1762
+ state.entries[index].active = false;
1763
+ syncScrollLock(state, win);
1764
+ }
1765
+
1766
+ const SCROLL_LOCK_KEY = '__SUI_SCROLL_LOCK__';
1767
+ function getStoredScrollLockState(win) {
1768
+ return win[SCROLL_LOCK_KEY];
1769
+ }
1770
+ function setStoredScrollLockState(win, state) {
1771
+ const global = win;
1772
+ global[SCROLL_LOCK_KEY] = state;
1773
+ return state;
1774
+ }
1775
+ function getOrCreateScrollLockState(win) {
1776
+ const state = getStoredScrollLockState(win);
1777
+ if (!state) {
1778
+ return setStoredScrollLockState(win, createInitialState());
1779
+ }
1780
+ return state;
1781
+ }
1782
+
1783
+ function applyLockState(state, win, targetElement, options, disabled) {
1784
+ lock(state, win, targetElement, options);
1785
+ if (disabled) {
1786
+ pause(state, win, targetElement, options);
1787
+ }
1788
+ }
1789
+ function useResolvedElement(target) {
1790
+ const [resolvedElement, setResolvedElement] = useState(() => (getRefElement(target) ?? null));
1791
+ const targetRef = useRef(target);
1792
+ targetRef.current = target;
1793
+ useEffect(() => {
1794
+ const nextResolvedElement = (getRefElement(targetRef.current) ??
1795
+ null);
1796
+ setResolvedElement((current) => current === nextResolvedElement ? current : nextResolvedElement);
1797
+ });
1798
+ return resolvedElement;
1799
+ }
1482
1800
  const useBodyScrollLock = (isLocked, targetElement, options = {}) => {
1483
- const { disabled = false, reserveScrollBarGap = true, allowTouchMove, } = options;
1484
- const { document } = useWindowContext();
1801
+ const { disabled = false, reserveScrollBarGap = true } = options;
1802
+ const { allowTouchMove } = options;
1803
+ const entryKeyRef = useRef(Symbol('use-body-scroll-lock-entry'));
1804
+ const resolvedElement = useResolvedElement(targetElement);
1805
+ const { globalThis: global } = useWindowContext();
1485
1806
  useEffect(() => {
1486
- if (disabled || !document?.body)
1807
+ if (!isLocked)
1487
1808
  return;
1488
- const element = targetElement || document.body;
1489
- if (isLocked) {
1490
- disableBodyScroll(element, {
1491
- reserveScrollBarGap,
1492
- allowTouchMove,
1493
- });
1494
- }
1809
+ const win = global;
1810
+ const state = getOrCreateScrollLockState(global);
1811
+ const entryKey = entryKeyRef.current;
1812
+ const syncOptions = {
1813
+ reserveScrollBarGap,
1814
+ allowTouchMove,
1815
+ entryKey,
1816
+ };
1817
+ applyLockState(state, win, resolvedElement, syncOptions, disabled);
1495
1818
  return () => {
1496
- if (isLocked) {
1497
- enableBodyScroll(element);
1819
+ const currentState = getStoredScrollLockState(global);
1820
+ if (currentState) {
1821
+ unlock(currentState, win, resolvedElement, { entryKey });
1498
1822
  }
1499
1823
  };
1824
+ }, [isLocked, global]);
1825
+ useEffect(() => {
1826
+ if (!isLocked)
1827
+ return;
1828
+ const win = global;
1829
+ const state = getOrCreateScrollLockState(global);
1830
+ const entryKey = entryKeyRef.current;
1831
+ const syncOptions = {
1832
+ reserveScrollBarGap,
1833
+ allowTouchMove,
1834
+ entryKey,
1835
+ };
1836
+ applyLockState(state, win, resolvedElement, syncOptions, disabled);
1500
1837
  }, [
1501
- isLocked,
1502
- targetElement,
1838
+ allowTouchMove,
1503
1839
  disabled,
1840
+ isLocked,
1841
+ global,
1842
+ resolvedElement,
1504
1843
  reserveScrollBarGap,
1505
- allowTouchMove,
1506
- document,
1507
1844
  ]);
1508
1845
  };
1509
1846
 
package/index.js CHANGED
@@ -13,5 +13,4 @@ import 'lodash/isPlainObject';
13
13
  import 'lodash/debounce';
14
14
  import 'lodash/noop';
15
15
  import 'lodash/throttle';
16
- import 'body-scroll-lock';
17
16
  import 'tailwind-merge';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ringcentral/spring-base",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "author": "RingCentral",
5
5
  "description": "Core utilities, hooks, and foundational functionality for Spring UI components, providing essential building blocks for React applications",
6
6
  "main": "./index.js",
@@ -9,7 +9,6 @@
9
9
  "dependencies": {
10
10
  "@mui/utils": "^6.0.2",
11
11
  "@types/lodash": "^4.14.182",
12
- "body-scroll-lock": "4.0.0-beta.0",
13
12
  "clsx": "^1.2.1",
14
13
  "framer-motion": "^6.5.1",
15
14
  "lodash": "^4.17.21",
@@ -0,0 +1,31 @@
1
+ declare type Win = Window & typeof globalThis;
2
+ export interface ScrollLockOptions {
3
+ reserveScrollBarGap?: boolean;
4
+ allowTouchMove?: (el: Element) => boolean;
5
+ entryKey?: symbol;
6
+ }
7
+ interface LockEntry {
8
+ key: symbol;
9
+ kind: 'anonymous' | 'element';
10
+ element: HTMLElement | null;
11
+ active: boolean;
12
+ reserveScrollBarGap: boolean;
13
+ allowTouchMove?: (el: Element) => boolean;
14
+ }
15
+ export interface WindowState {
16
+ entries: LockEntry[];
17
+ savedBodyOverflow: string;
18
+ savedBodyPaddingRight: string | null;
19
+ docTouchStartHandler: ((e: TouchEvent) => void) | null;
20
+ docTouchMoveHandler: ((e: TouchEvent) => void) | null;
21
+ touchStartY: number;
22
+ touchStartX: number;
23
+ nonIOSLocked: boolean;
24
+ nonIOSReservedScrollBarGap: boolean;
25
+ }
26
+ export declare function createInitialState(): WindowState;
27
+ export declare function lock(state: WindowState, win: Win, targetElement?: HTMLElement | null, options?: ScrollLockOptions): void;
28
+ export declare function unlock(state: WindowState, win: Win, targetElement?: HTMLElement | null, options?: ScrollLockOptions): void;
29
+ export declare function pause(state: WindowState, win: Win, targetElement?: HTMLElement | null, options?: ScrollLockOptions): void;
30
+ export declare function clearAllLocks(state: WindowState, win: Win): void;
31
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { WindowState } from './bodyScrollLock';
2
+ export declare const SCROLL_LOCK_KEY = "__SUI_SCROLL_LOCK__";
3
+ export declare function getStoredScrollLockState(win: typeof globalThis): WindowState | undefined;
4
+ export declare function setStoredScrollLockState(win: typeof globalThis, state: WindowState): WindowState;
5
+ export declare function getOrCreateScrollLockState(win: typeof globalThis): WindowState;
6
+ export declare function deleteStoredScrollLockState(win: typeof globalThis): void;
@@ -1,6 +1,8 @@
1
+ import { RefObject } from 'react';
2
+ export declare type UseBodyScrollLockTarget = HTMLElement | RefObject<HTMLElement | null> | null | undefined;
1
3
  export interface UseBodyScrollLockOptions {
2
4
  disabled?: boolean;
3
5
  reserveScrollBarGap?: boolean;
4
6
  allowTouchMove?: (el: Element) => boolean;
5
7
  }
6
- export declare const useBodyScrollLock: (isLocked: boolean, targetElement?: HTMLElement | null | undefined, options?: UseBodyScrollLockOptions) => void;
8
+ export declare const useBodyScrollLock: (isLocked: boolean, targetElement?: UseBodyScrollLockTarget, options?: UseBodyScrollLockOptions) => void;
@@ -8,6 +8,7 @@ export declare type ClassesProp<K extends string> = {
8
8
  classes?: Partial<Record<K | 'root' | GlobalStateClassesKey, string>>;
9
9
  };
10
10
  export declare type PropsWithRoot<P, R extends ElementType, O extends string = '', E = HTMLDataAttribute> = P & Omit<ComponentPropsWithRef<R>, keyof P | O> & E;
11
+ export declare type SlotProps<R extends ElementType, O extends keyof ComponentPropsWithRef<R> = never, E = HTMLDataAttribute> = Omit<ComponentPropsWithRef<R>, O | 'className'> & E;
11
12
  export declare type StandardProps<P, C extends string, R extends ElementType | void = void, O extends string = '', ExtendAttributes = HTMLDataAttribute> = ClassesProp<C> & (R extends ElementType ? PropsWithRoot<P, R, 'classes' | O, ExtendAttributes> : P & {
12
13
  className?: string;
13
14
  }) & ExtendAttributes;