ripple 0.2.207 → 0.2.208

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.207",
6
+ "version": "0.2.208",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -97,6 +97,6 @@
97
97
  "vscode-languageserver-types": "^3.17.5"
98
98
  },
99
99
  "peerDependencies": {
100
- "ripple": "0.2.207"
100
+ "ripple": "0.2.208"
101
101
  }
102
102
  }
@@ -248,15 +248,35 @@ export function bindValue(maybe_tracked, set_func = undefined) {
248
248
  });
249
249
  } else {
250
250
  var input = /** @type {HTMLInputElement} */ (node);
251
- var selection_restore_needed = false;
252
251
 
253
252
  clear_event = on(input, 'input', () => {
254
- selection_restore_needed = true;
255
253
  /** @type {any} */
256
254
  var value = input.value;
257
255
  value = is_numberlike_input(input) ? to_number(value) : value;
258
- // the setter will schedule a microtask and the render block below will run
259
256
  setter(value);
257
+ const getter_value = getter();
258
+
259
+ // Check the getter to see if it's different from the input.value
260
+ // The setter may have decided not to update its track value or update it to something else
261
+ // We treat the getter as the source of truth since we cannot verify the change otherwise
262
+ // If getter() !== input.value, we set the input value right away
263
+ // the `render` block may be scheduled only if the tracked value has changed
264
+ // but it will not do anything if getter() === input.value
265
+ // The result is: the `render` block will ALWAYS exit early if the microtask
266
+ // came from this event handler
267
+ if (value !== getter_value) {
268
+ var start = input.selectionStart;
269
+ var end = input.selectionEnd;
270
+
271
+ input.value = getter_value ?? '';
272
+
273
+ if (end !== null && start !== null) {
274
+ end = Math.min(end, input.value.length);
275
+ start = Math.min(start, end);
276
+ input.selectionStart = start;
277
+ input.selectionEnd = end;
278
+ }
279
+ }
260
280
  });
261
281
 
262
282
  render(() => {
@@ -271,23 +291,9 @@ export function bindValue(maybe_tracked, set_func = undefined) {
271
291
  }
272
292
 
273
293
  if (value !== input.value) {
274
- if (selection_restore_needed) {
275
- var start = input.selectionStart;
276
- var end = input.selectionEnd;
277
-
278
- input.value = value ?? '';
279
-
280
- // Restore selection
281
- if (end !== null && start !== null) {
282
- end = Math.min(end, input.value.length);
283
- start = Math.min(start, end);
284
- input.selectionStart = start;
285
- input.selectionEnd = end;
286
- }
287
- selection_restore_needed = false;
288
- } else {
289
- input.value = value ?? '';
290
- }
294
+ // this can only get here if the tracked value was changed directly,
295
+ // and not via the input event
296
+ input.value = value ?? '';
291
297
  }
292
298
  });
293
299
 
@@ -1516,207 +1516,389 @@ describe('use value()', () => {
1516
1516
  // Should fall back to first non-disabled option
1517
1517
  expect(select.options[1].selected).toBe(true);
1518
1518
  });
1519
- });
1520
-
1521
- describe('bindClientWidth and bindClientHeight', () => {
1522
- it('should bind element clientWidth', () => {
1523
- const logs: number[] = [];
1524
1519
 
1520
+ it('should accurately reflect values mutated through a tracked setter', () => {
1525
1521
  component App() {
1526
- const width = track(0);
1527
-
1528
- effect(() => {
1529
- logs.push(@width);
1530
- });
1522
+ let value = track(
1523
+ '',
1524
+ (val) => {
1525
+ return val;
1526
+ },
1527
+ (next) => {
1528
+ if (next.includes('c')) {
1529
+ next = next.replace(/c/g, '');
1530
+ }
1531
+ return next;
1532
+ },
1533
+ );
1531
1534
 
1532
- <div {ref bindClientWidth(width)} />
1535
+ <input type="text" {ref bindValue(value)} />
1536
+ <div>{@value}</div>
1533
1537
  }
1534
1538
 
1535
1539
  render(App);
1536
1540
  flushSync();
1537
1541
 
1542
+ const input = container.querySelector('input') as HTMLInputElement;
1538
1543
  const div = container.querySelector('div') as HTMLDivElement;
1539
1544
 
1540
- Object.defineProperty(div, 'clientWidth', {
1541
- configurable: true,
1542
- get: () => 200,
1543
- });
1545
+ expect(input.value).toBe('');
1546
+ expect(div.textContent).toBe('');
1544
1547
 
1545
- triggerResize(div, {
1546
- contentRect: new DOMRectReadOnly(0, 0, 200, 100),
1547
- });
1548
+ input.value = 'abc';
1549
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1548
1550
  flushSync();
1549
1551
 
1550
- expect(logs[logs.length - 1]).toBe(200);
1552
+ expect(input.value).toBe('ab');
1553
+ expect(div.textContent).toBe('ab');
1551
1554
  });
1552
1555
 
1553
- it('should bind element clientWidth with a getter and setter', () => {
1554
- const logs: number[] = [];
1555
-
1556
+ it('should accurately reflect values when a getter modifies value', () => {
1556
1557
  component App() {
1557
- const width = #{ value: 0 };
1558
-
1559
- effect(() => {
1560
- logs.push(width.value);
1561
- });
1558
+ let value = track(
1559
+ '',
1560
+ (val) => {
1561
+ if (val.includes('c')) {
1562
+ val = val.replace(/c/g, '');
1563
+ }
1564
+ return val;
1565
+ },
1566
+ (next) => {
1567
+ return next;
1568
+ },
1569
+ );
1562
1570
 
1563
- <div {ref bindClientWidth(() => width.value, (v: number) => (width.value = v))} />
1571
+ <input type="text" {ref bindValue(value)} />
1572
+ <div>{@value}</div>
1564
1573
  }
1565
1574
 
1566
1575
  render(App);
1567
1576
  flushSync();
1568
1577
 
1578
+ const input = container.querySelector('input') as HTMLInputElement;
1569
1579
  const div = container.querySelector('div') as HTMLDivElement;
1570
1580
 
1571
- Object.defineProperty(div, 'clientWidth', {
1572
- configurable: true,
1573
- get: () => 200,
1574
- });
1581
+ expect(input.value).toBe('');
1582
+ expect(div.textContent).toBe('');
1575
1583
 
1576
- triggerResize(div, {
1577
- contentRect: new DOMRectReadOnly(0, 0, 200, 100),
1578
- });
1584
+ input.value = 'abc';
1585
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1579
1586
  flushSync();
1580
1587
 
1581
- expect(logs[logs.length - 1]).toBe(200);
1588
+ expect(input.value).toBe('ab');
1589
+ expect(div.textContent).toBe('ab');
1582
1590
  });
1583
1591
 
1584
- it('should bind element clientHeight', () => {
1585
- const logs: number[] = [];
1586
-
1592
+ it('should always prefer what getter returns even if setter mutates next', () => {
1587
1593
  component App() {
1588
- const height = track(0);
1589
-
1590
- effect(() => {
1591
- logs.push(@height);
1592
- });
1594
+ let value = track(
1595
+ '',
1596
+ (val) => {
1597
+ return val.replace(/[c,b]+/g, '');
1598
+ },
1599
+ (next) => {
1600
+ if (next.includes('c')) {
1601
+ next = next.replace(/c/g, '');
1602
+ }
1603
+ return next;
1604
+ },
1605
+ );
1593
1606
 
1594
- <div {ref bindClientHeight(height)} />
1607
+ <input type="text" {ref bindValue(value)} />
1608
+ <div>{@value}</div>
1595
1609
  }
1596
1610
 
1597
1611
  render(App);
1598
1612
  flushSync();
1599
1613
 
1614
+ const input = container.querySelector('input') as HTMLInputElement;
1600
1615
  const div = container.querySelector('div') as HTMLDivElement;
1601
1616
 
1602
- Object.defineProperty(div, 'clientHeight', {
1603
- configurable: true,
1604
- get: () => 150,
1605
- });
1617
+ expect(input.value).toBe('');
1618
+ expect(div.textContent).toBe('');
1606
1619
 
1607
- triggerResize(div, {
1608
- contentRect: new DOMRectReadOnly(0, 0, 100, 150),
1609
- });
1620
+ input.value = 'abc';
1621
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1610
1622
  flushSync();
1611
1623
 
1612
- expect(logs[logs.length - 1]).toBe(150);
1624
+ expect(input.value).toBe('a');
1625
+ expect(div.textContent).toBe('a');
1613
1626
  });
1614
1627
 
1615
- it('should bind element clientHeight with a getter and setter', () => {
1616
- const logs: number[] = [];
1617
-
1618
- component App() {
1619
- const height = #{ value: 0 };
1620
- effect(() => {
1621
- logs.push(height.value);
1622
- });
1628
+ it(
1629
+ 'should accurately reflect values mutated through an effect even after a setter mutation',
1630
+ () => {
1631
+ component App() {
1632
+ let value = track(
1633
+ '',
1634
+ (val) => {
1635
+ return val;
1636
+ },
1637
+ (next) => {
1638
+ if (next.includes('c')) {
1639
+ next = next.replace(/c/g, '');
1640
+ }
1641
+ return next;
1642
+ },
1643
+ );
1623
1644
 
1624
- <div {ref bindClientHeight(() => height.value, (v: number) => (height.value = v))} />
1625
- }
1645
+ effect(() => {
1646
+ @value;
1626
1647
 
1627
- render(App);
1628
- flushSync();
1648
+ untrack(() => {
1649
+ if (@value.includes('a')) {
1650
+ @value = @value.replace(/a/g, '');
1651
+ }
1652
+ });
1653
+ });
1654
+ <input type="text" {ref bindValue(value)} />
1655
+ <div>{@value}</div>
1656
+ }
1629
1657
 
1630
- const div = container.querySelector('div') as HTMLDivElement;
1658
+ render(App);
1659
+ flushSync();
1631
1660
 
1632
- Object.defineProperty(div, 'clientHeight', {
1633
- configurable: true,
1634
- get: () => 150,
1635
- });
1661
+ const input = container.querySelector('input') as HTMLInputElement;
1662
+ const div = container.querySelector('div') as HTMLDivElement;
1636
1663
 
1637
- triggerResize(div, {
1638
- contentRect: new DOMRectReadOnly(0, 0, 100, 150),
1639
- });
1640
- flushSync();
1664
+ expect(input.value).toBe('');
1665
+ expect(div.textContent).toBe('');
1641
1666
 
1642
- expect(logs[logs.length - 1]).toBe(150);
1643
- });
1644
- });
1667
+ input.value = 'abc';
1668
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1669
+ flushSync();
1645
1670
 
1646
- describe('bindOffsetWidth and bindOffsetHeight', () => {
1647
- it('should bind element offsetWidth', () => {
1648
- const logs: number[] = [];
1671
+ expect(input.value).toBe('b');
1672
+ expect(div.textContent).toBe('b');
1673
+ },
1674
+ );
1649
1675
 
1676
+ it('should accurately reflect values mutated through a tracked setter via bind accessors', () => {
1650
1677
  component App() {
1651
- const width = track(0);
1652
-
1653
- effect(() => {
1654
- logs.push(@width);
1655
- });
1678
+ let value = track('');
1679
+ const value_accessors = [
1680
+ () => {
1681
+ return @value;
1682
+ },
1683
+ (v: string) => {
1684
+ if (v.includes('c')) {
1685
+ v = v.replace(/c/g, '');
1686
+ }
1687
+ @value = v;
1688
+ },
1689
+ ];
1656
1690
 
1657
- <div {ref bindOffsetWidth(width)} />
1691
+ <input type="text" {ref bindValue(...value_accessors)} />
1658
1692
  }
1659
1693
 
1660
1694
  render(App);
1661
1695
  flushSync();
1662
1696
 
1663
- const div = container.querySelector('div') as HTMLDivElement;
1697
+ const input = container.querySelector('input') as HTMLInputElement;
1664
1698
 
1665
- Object.defineProperty(div, 'offsetWidth', {
1666
- configurable: true,
1667
- get: () => 250,
1668
- });
1699
+ expect(input.value).toBe('');
1669
1700
 
1670
- triggerResize(div, {
1671
- contentRect: new DOMRectReadOnly(0, 0, 250, 100),
1672
- });
1701
+ input.value = 'abc';
1702
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1673
1703
  flushSync();
1674
1704
 
1675
- expect(logs[logs.length - 1]).toBe(250);
1705
+ expect(input.value).toBe('ab');
1676
1706
  });
1677
1707
 
1678
- it('should bind element offsetWidth with a getter and setter', () => {
1679
- const logs: number[] = [];
1680
-
1708
+ it('should prefer what getter returns via bind accessors', () => {
1681
1709
  component App() {
1682
- const width = #{ value: 0 };
1683
-
1684
- effect(() => {
1685
- logs.push(width.value);
1686
- });
1710
+ let value = track('');
1711
+ const value_accessors = [
1712
+ () => {
1713
+ if (@value.includes('c')) {
1714
+ return @value.replace(/c/g, '');
1715
+ }
1716
+ return @value;
1717
+ },
1718
+ (v: string) => {
1719
+ @value = v;
1720
+ },
1721
+ ];
1687
1722
 
1688
- <div {ref bindOffsetWidth(() => width.value, (v: number) => (width.value = v))} />
1723
+ <input type="text" {ref bindValue(...value_accessors)} />
1689
1724
  }
1690
1725
 
1691
1726
  render(App);
1692
1727
  flushSync();
1693
1728
 
1694
- const div = container.querySelector('div') as HTMLDivElement;
1729
+ const input = container.querySelector('input') as HTMLInputElement;
1695
1730
 
1696
- Object.defineProperty(div, 'offsetWidth', {
1697
- configurable: true,
1698
- get: () => 250,
1699
- });
1731
+ expect(input.value).toBe('');
1700
1732
 
1701
- triggerResize(div, {
1702
- contentRect: new DOMRectReadOnly(0, 0, 250, 100),
1703
- });
1733
+ input.value = 'abc';
1734
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1704
1735
  flushSync();
1705
1736
 
1706
- expect(logs[logs.length - 1]).toBe(250);
1737
+ expect(input.value).toBe('ab');
1707
1738
  });
1708
1739
 
1709
- it('should bind element offsetHeight', () => {
1740
+ it(
1741
+ 'should always prefer what getter returns even if setter mutates next via bind accessors',
1742
+ () => {
1743
+ component App() {
1744
+ let value = track('');
1745
+ const value_accessors = [
1746
+ () => {
1747
+ return @value.replace(/[c,b]+/g, '');
1748
+ },
1749
+ (v: string) => {
1750
+ if (v.includes('c')) {
1751
+ v = v.replace(/c/g, '');
1752
+ }
1753
+ @value = v;
1754
+ },
1755
+ ];
1756
+
1757
+ <input type="text" {ref bindValue(...value_accessors)} />
1758
+ }
1759
+
1760
+ render(App);
1761
+ flushSync();
1762
+
1763
+ const input = container.querySelector('input') as HTMLInputElement;
1764
+
1765
+ expect(input.value).toBe('');
1766
+
1767
+ input.value = 'abc';
1768
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1769
+ flushSync();
1770
+
1771
+ expect(input.value).toBe('a');
1772
+ },
1773
+ );
1774
+
1775
+ it(
1776
+ 'should accurately reflect values mutated through an effect even after a setter mutation via bind accessors',
1777
+ () => {
1778
+ component App() {
1779
+ let value = track('');
1780
+ const value_accessors = [
1781
+ () => {
1782
+ return @value;
1783
+ },
1784
+ (v: string) => {
1785
+ if (v.includes('c')) {
1786
+ v = v.replace(/c/g, '');
1787
+ }
1788
+ @value = v;
1789
+ },
1790
+ ];
1791
+
1792
+ effect(() => {
1793
+ @value;
1794
+
1795
+ untrack(() => {
1796
+ if (@value.includes('a')) {
1797
+ @value = @value.replace(/a/g, '');
1798
+ }
1799
+ });
1800
+ });
1801
+ <input type="text" {ref bindValue(...value_accessors)} />
1802
+ }
1803
+
1804
+ render(App);
1805
+ flushSync();
1806
+
1807
+ const input = container.querySelector('input') as HTMLInputElement;
1808
+
1809
+ expect(input.value).toBe('');
1810
+
1811
+ input.value = 'abc';
1812
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1813
+ flushSync();
1814
+
1815
+ expect(input.value).toBe('b');
1816
+ },
1817
+ );
1818
+
1819
+ it(
1820
+ 'should keep the input.value unchanged and synced with the tracked when the setter blocks updates to the tracked via bind accessors',
1821
+ () => {
1822
+ component App() {
1823
+ let value = track('');
1824
+ const value_accessors = [
1825
+ () => {
1826
+ return @value;
1827
+ },
1828
+ (v: string) => {
1829
+ // no update
1830
+ },
1831
+ ];
1832
+
1833
+ <input type="text" {ref bindValue(...value_accessors)} />
1834
+ <div>{@value}</div>
1835
+ }
1836
+
1837
+ render(App);
1838
+ flushSync();
1839
+
1840
+ const input = container.querySelector('input') as HTMLInputElement;
1841
+ const div = container.querySelector('div') as HTMLDivElement;
1842
+
1843
+ expect(input.value).toBe('');
1844
+
1845
+ input.value = 'abc';
1846
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1847
+ flushSync();
1848
+
1849
+ expect(input.value).toBe('');
1850
+ expect(div.textContent).toBe('');
1851
+ },
1852
+ );
1853
+
1854
+ it(
1855
+ 'should keep the input.value unchanged and synced with the tracked when the setter blocks updates to the tracked via track get / set',
1856
+ () => {
1857
+ component App() {
1858
+ let value = track(
1859
+ '',
1860
+ (v) => {
1861
+ return v;
1862
+ },
1863
+ () => {
1864
+ return '';
1865
+ },
1866
+ );
1867
+
1868
+ <input type="text" {ref bindValue(value)} />
1869
+ <div>{@value}</div>
1870
+ }
1871
+
1872
+ render(App);
1873
+ flushSync();
1874
+
1875
+ const input = container.querySelector('input') as HTMLInputElement;
1876
+ const div = container.querySelector('div') as HTMLDivElement;
1877
+
1878
+ expect(input.value).toBe('');
1879
+
1880
+ input.value = 'abc';
1881
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1882
+ flushSync();
1883
+
1884
+ expect(input.value).toBe('');
1885
+ expect(div.textContent).toBe('');
1886
+ },
1887
+ );
1888
+ });
1889
+
1890
+ describe('bindClientWidth and bindClientHeight', () => {
1891
+ it('should bind element clientWidth', () => {
1710
1892
  const logs: number[] = [];
1711
1893
 
1712
1894
  component App() {
1713
- const height = track(0);
1895
+ const width = track(0);
1714
1896
 
1715
1897
  effect(() => {
1716
- logs.push(@height);
1898
+ logs.push(@width);
1717
1899
  });
1718
1900
 
1719
- <div {ref bindOffsetHeight(height)} />
1901
+ <div {ref bindClientWidth(width)} />
1720
1902
  }
1721
1903
 
1722
1904
  render(App);
@@ -1724,29 +1906,30 @@ describe('bindOffsetWidth and bindOffsetHeight', () => {
1724
1906
 
1725
1907
  const div = container.querySelector('div') as HTMLDivElement;
1726
1908
 
1727
- Object.defineProperty(div, 'offsetHeight', {
1909
+ Object.defineProperty(div, 'clientWidth', {
1728
1910
  configurable: true,
1729
- get: () => 175,
1911
+ get: () => 200,
1730
1912
  });
1731
1913
 
1732
1914
  triggerResize(div, {
1733
- contentRect: new DOMRectReadOnly(0, 0, 100, 175),
1915
+ contentRect: new DOMRectReadOnly(0, 0, 200, 100),
1734
1916
  });
1735
1917
  flushSync();
1736
1918
 
1737
- expect(logs[logs.length - 1]).toBe(175);
1919
+ expect(logs[logs.length - 1]).toBe(200);
1738
1920
  });
1739
1921
 
1740
- it('should bind element offsetHeight with a getter and setter', () => {
1922
+ it('should bind element clientWidth with a getter and setter', () => {
1741
1923
  const logs: number[] = [];
1742
1924
 
1743
1925
  component App() {
1744
- const height = #{ value: 0 };
1926
+ const width = #{ value: 0 };
1927
+
1745
1928
  effect(() => {
1746
- logs.push(height.value);
1929
+ logs.push(width.value);
1747
1930
  });
1748
1931
 
1749
- <div {ref bindOffsetHeight(() => height.value, (v: number) => (height.value = v))} />
1932
+ <div {ref bindClientWidth(() => width.value, (v: number) => (width.value = v))} />
1750
1933
  }
1751
1934
 
1752
1935
  render(App);
@@ -1754,32 +1937,30 @@ describe('bindOffsetWidth and bindOffsetHeight', () => {
1754
1937
 
1755
1938
  const div = container.querySelector('div') as HTMLDivElement;
1756
1939
 
1757
- Object.defineProperty(div, 'offsetHeight', {
1940
+ Object.defineProperty(div, 'clientWidth', {
1758
1941
  configurable: true,
1759
- get: () => 175,
1942
+ get: () => 200,
1760
1943
  });
1761
1944
 
1762
1945
  triggerResize(div, {
1763
- contentRect: new DOMRectReadOnly(0, 0, 100, 175),
1946
+ contentRect: new DOMRectReadOnly(0, 0, 200, 100),
1764
1947
  });
1765
1948
  flushSync();
1766
1949
 
1767
- expect(logs[logs.length - 1]).toBe(175);
1950
+ expect(logs[logs.length - 1]).toBe(200);
1768
1951
  });
1769
- });
1770
1952
 
1771
- describe('bindContentRect', () => {
1772
- it('should bind element contentRect', () => {
1773
- const logs: DOMRectReadOnly[] = [];
1953
+ it('should bind element clientHeight', () => {
1954
+ const logs: number[] = [];
1774
1955
 
1775
1956
  component App() {
1776
- const rect = track(null);
1957
+ const height = track(0);
1777
1958
 
1778
1959
  effect(() => {
1779
- if (@rect) logs.push(@rect);
1960
+ logs.push(@height);
1780
1961
  });
1781
1962
 
1782
- <div {ref bindContentRect(rect)} />
1963
+ <div {ref bindClientHeight(height)} />
1783
1964
  }
1784
1965
 
1785
1966
  render(App);
@@ -1787,33 +1968,29 @@ describe('bindContentRect', () => {
1787
1968
 
1788
1969
  const div = container.querySelector('div') as HTMLDivElement;
1789
1970
 
1790
- const mockRect = new DOMRectReadOnly(10, 20, 300, 200);
1971
+ Object.defineProperty(div, 'clientHeight', {
1972
+ configurable: true,
1973
+ get: () => 150,
1974
+ });
1975
+
1791
1976
  triggerResize(div, {
1792
- contentRect: mockRect,
1977
+ contentRect: new DOMRectReadOnly(0, 0, 100, 150),
1793
1978
  });
1794
1979
  flushSync();
1795
1980
 
1796
- expect(logs.length).toBeGreaterThan(0);
1797
- const lastRect = logs[logs.length - 1];
1798
- expect(lastRect.width).toBe(300);
1799
- expect(lastRect.height).toBe(200);
1981
+ expect(logs[logs.length - 1]).toBe(150);
1800
1982
  });
1801
1983
 
1802
- it('should bind element contentRect with a getter and setter', () => {
1803
- const logs: DOMRectReadOnly[] = [];
1984
+ it('should bind element clientHeight with a getter and setter', () => {
1985
+ const logs: number[] = [];
1804
1986
 
1805
1987
  component App() {
1806
- const rect: TrackedObject<{ value: DOMRectReadOnly | null }> = #{ value: null };
1988
+ const height = #{ value: 0 };
1807
1989
  effect(() => {
1808
- if (rect.value) logs.push(rect.value);
1990
+ logs.push(height.value);
1809
1991
  });
1810
1992
 
1811
- <div
1812
- {ref bindContentRect<null | DOMRectReadOnly>(
1813
- () => rect.value,
1814
- (v: DOMRectReadOnly) => (rect.value = v),
1815
- )}
1816
- />
1993
+ <div {ref bindClientHeight(() => height.value, (v: number) => (height.value = v))} />
1817
1994
  }
1818
1995
 
1819
1996
  render(App);
@@ -1821,31 +1998,32 @@ describe('bindContentRect', () => {
1821
1998
 
1822
1999
  const div = container.querySelector('div') as HTMLDivElement;
1823
2000
 
1824
- const mockRect = new DOMRectReadOnly(10, 20, 300, 200);
2001
+ Object.defineProperty(div, 'clientHeight', {
2002
+ configurable: true,
2003
+ get: () => 150,
2004
+ });
2005
+
1825
2006
  triggerResize(div, {
1826
- contentRect: mockRect,
2007
+ contentRect: new DOMRectReadOnly(0, 0, 100, 150),
1827
2008
  });
1828
2009
  flushSync();
1829
2010
 
1830
- expect(logs.length).toBeGreaterThan(0);
1831
- const lastRect = logs[logs.length - 1];
1832
- expect(lastRect.width).toBe(300);
1833
- expect(lastRect.height).toBe(200);
2011
+ expect(logs[logs.length - 1]).toBe(150);
1834
2012
  });
1835
2013
  });
1836
2014
 
1837
- describe('bindContentBoxSize', () => {
1838
- it('should bind element contentBoxSize', () => {
1839
- const logs: any[] = [];
2015
+ describe('bindOffsetWidth and bindOffsetHeight', () => {
2016
+ it('should bind element offsetWidth', () => {
2017
+ const logs: number[] = [];
1840
2018
 
1841
2019
  component App() {
1842
- const boxSize = track(null);
2020
+ const width = track(0);
1843
2021
 
1844
2022
  effect(() => {
1845
- if (@boxSize) logs.push(@boxSize);
2023
+ logs.push(@width);
1846
2024
  });
1847
2025
 
1848
- <div {ref bindContentBoxSize(boxSize)} />
2026
+ <div {ref bindOffsetWidth(width)} />
1849
2027
  }
1850
2028
 
1851
2029
  render(App);
@@ -1853,16 +2031,207 @@ describe('bindContentBoxSize', () => {
1853
2031
 
1854
2032
  const div = container.querySelector('div') as HTMLDivElement;
1855
2033
 
1856
- const mockBoxSize = [
1857
- { blockSize: 200, inlineSize: 300 },
1858
- ];
2034
+ Object.defineProperty(div, 'offsetWidth', {
2035
+ configurable: true,
2036
+ get: () => 250,
2037
+ });
2038
+
1859
2039
  triggerResize(div, {
1860
- contentBoxSize: mockBoxSize as any,
2040
+ contentRect: new DOMRectReadOnly(0, 0, 250, 100),
1861
2041
  });
1862
2042
  flushSync();
1863
2043
 
1864
- expect(logs.length).toBeGreaterThan(0);
1865
- expect(logs[logs.length - 1]).toBe(mockBoxSize);
2044
+ expect(logs[logs.length - 1]).toBe(250);
2045
+ });
2046
+
2047
+ it('should bind element offsetWidth with a getter and setter', () => {
2048
+ const logs: number[] = [];
2049
+
2050
+ component App() {
2051
+ const width = #{ value: 0 };
2052
+
2053
+ effect(() => {
2054
+ logs.push(width.value);
2055
+ });
2056
+
2057
+ <div {ref bindOffsetWidth(() => width.value, (v: number) => (width.value = v))} />
2058
+ }
2059
+
2060
+ render(App);
2061
+ flushSync();
2062
+
2063
+ const div = container.querySelector('div') as HTMLDivElement;
2064
+
2065
+ Object.defineProperty(div, 'offsetWidth', {
2066
+ configurable: true,
2067
+ get: () => 250,
2068
+ });
2069
+
2070
+ triggerResize(div, {
2071
+ contentRect: new DOMRectReadOnly(0, 0, 250, 100),
2072
+ });
2073
+ flushSync();
2074
+
2075
+ expect(logs[logs.length - 1]).toBe(250);
2076
+ });
2077
+
2078
+ it('should bind element offsetHeight', () => {
2079
+ const logs: number[] = [];
2080
+
2081
+ component App() {
2082
+ const height = track(0);
2083
+
2084
+ effect(() => {
2085
+ logs.push(@height);
2086
+ });
2087
+
2088
+ <div {ref bindOffsetHeight(height)} />
2089
+ }
2090
+
2091
+ render(App);
2092
+ flushSync();
2093
+
2094
+ const div = container.querySelector('div') as HTMLDivElement;
2095
+
2096
+ Object.defineProperty(div, 'offsetHeight', {
2097
+ configurable: true,
2098
+ get: () => 175,
2099
+ });
2100
+
2101
+ triggerResize(div, {
2102
+ contentRect: new DOMRectReadOnly(0, 0, 100, 175),
2103
+ });
2104
+ flushSync();
2105
+
2106
+ expect(logs[logs.length - 1]).toBe(175);
2107
+ });
2108
+
2109
+ it('should bind element offsetHeight with a getter and setter', () => {
2110
+ const logs: number[] = [];
2111
+
2112
+ component App() {
2113
+ const height = #{ value: 0 };
2114
+ effect(() => {
2115
+ logs.push(height.value);
2116
+ });
2117
+
2118
+ <div {ref bindOffsetHeight(() => height.value, (v: number) => (height.value = v))} />
2119
+ }
2120
+
2121
+ render(App);
2122
+ flushSync();
2123
+
2124
+ const div = container.querySelector('div') as HTMLDivElement;
2125
+
2126
+ Object.defineProperty(div, 'offsetHeight', {
2127
+ configurable: true,
2128
+ get: () => 175,
2129
+ });
2130
+
2131
+ triggerResize(div, {
2132
+ contentRect: new DOMRectReadOnly(0, 0, 100, 175),
2133
+ });
2134
+ flushSync();
2135
+
2136
+ expect(logs[logs.length - 1]).toBe(175);
2137
+ });
2138
+ });
2139
+
2140
+ describe('bindContentRect', () => {
2141
+ it('should bind element contentRect', () => {
2142
+ const logs: DOMRectReadOnly[] = [];
2143
+
2144
+ component App() {
2145
+ const rect = track(null);
2146
+
2147
+ effect(() => {
2148
+ if (@rect) logs.push(@rect);
2149
+ });
2150
+
2151
+ <div {ref bindContentRect(rect)} />
2152
+ }
2153
+
2154
+ render(App);
2155
+ flushSync();
2156
+
2157
+ const div = container.querySelector('div') as HTMLDivElement;
2158
+
2159
+ const mockRect = new DOMRectReadOnly(10, 20, 300, 200);
2160
+ triggerResize(div, {
2161
+ contentRect: mockRect,
2162
+ });
2163
+ flushSync();
2164
+
2165
+ expect(logs.length).toBeGreaterThan(0);
2166
+ const lastRect = logs[logs.length - 1];
2167
+ expect(lastRect.width).toBe(300);
2168
+ expect(lastRect.height).toBe(200);
2169
+ });
2170
+
2171
+ it('should bind element contentRect with a getter and setter', () => {
2172
+ const logs: DOMRectReadOnly[] = [];
2173
+
2174
+ component App() {
2175
+ const rect: TrackedObject<{ value: DOMRectReadOnly | null }> = #{ value: null };
2176
+ effect(() => {
2177
+ if (rect.value) logs.push(rect.value);
2178
+ });
2179
+
2180
+ <div
2181
+ {ref bindContentRect<null | DOMRectReadOnly>(
2182
+ () => rect.value,
2183
+ (v: DOMRectReadOnly) => (rect.value = v),
2184
+ )}
2185
+ />
2186
+ }
2187
+
2188
+ render(App);
2189
+ flushSync();
2190
+
2191
+ const div = container.querySelector('div') as HTMLDivElement;
2192
+
2193
+ const mockRect = new DOMRectReadOnly(10, 20, 300, 200);
2194
+ triggerResize(div, {
2195
+ contentRect: mockRect,
2196
+ });
2197
+ flushSync();
2198
+
2199
+ expect(logs.length).toBeGreaterThan(0);
2200
+ const lastRect = logs[logs.length - 1];
2201
+ expect(lastRect.width).toBe(300);
2202
+ expect(lastRect.height).toBe(200);
2203
+ });
2204
+ });
2205
+
2206
+ describe('bindContentBoxSize', () => {
2207
+ it('should bind element contentBoxSize', () => {
2208
+ const logs: any[] = [];
2209
+
2210
+ component App() {
2211
+ const boxSize = track(null);
2212
+
2213
+ effect(() => {
2214
+ if (@boxSize) logs.push(@boxSize);
2215
+ });
2216
+
2217
+ <div {ref bindContentBoxSize(boxSize)} />
2218
+ }
2219
+
2220
+ render(App);
2221
+ flushSync();
2222
+
2223
+ const div = container.querySelector('div') as HTMLDivElement;
2224
+
2225
+ const mockBoxSize = [
2226
+ { blockSize: 200, inlineSize: 300 },
2227
+ ];
2228
+ triggerResize(div, {
2229
+ contentBoxSize: mockBoxSize as any,
2230
+ });
2231
+ flushSync();
2232
+
2233
+ expect(logs.length).toBeGreaterThan(0);
2234
+ expect(logs[logs.length - 1]).toBe(mockBoxSize);
1866
2235
  });
1867
2236
 
1868
2237
  it('should bind element contentBoxSize with a getter and setter', () => {
@@ -2534,305 +2903,6 @@ describe('bindNode', () => {
2534
2903
 
2535
2904
  expect(input.value).toBe('Set by ref');
2536
2905
  });
2537
-
2538
- it('should accurately reflect values mutated through a tracked setter', () => {
2539
- component App() {
2540
- let value = track(
2541
- '',
2542
- (val) => {
2543
- return val;
2544
- },
2545
- (next) => {
2546
- if (next.includes('c')) {
2547
- next = next.replace(/c/g, '');
2548
- }
2549
- return next;
2550
- },
2551
- );
2552
-
2553
- <input type="text" {ref bindValue(value)} />
2554
- <div>{@value}</div>
2555
- }
2556
-
2557
- render(App);
2558
- flushSync();
2559
-
2560
- const input = container.querySelector('input') as HTMLInputElement;
2561
- const div = container.querySelector('div') as HTMLDivElement;
2562
-
2563
- expect(input.value).toBe('');
2564
- expect(div.textContent).toBe('');
2565
-
2566
- input.value = 'abc';
2567
- input.dispatchEvent(new Event('input', { bubbles: true }));
2568
- flushSync();
2569
-
2570
- expect(input.value).toBe('ab');
2571
- expect(div.textContent).toBe('ab');
2572
- });
2573
-
2574
- it('should accurately reflect values when a getter modifies value', () => {
2575
- component App() {
2576
- let value = track(
2577
- '',
2578
- (val) => {
2579
- if (val.includes('c')) {
2580
- val = val.replace(/c/g, '');
2581
- }
2582
- return val;
2583
- },
2584
- (next) => {
2585
- return next;
2586
- },
2587
- );
2588
-
2589
- <input type="text" {ref bindValue(value)} />
2590
- <div>{@value}</div>
2591
- }
2592
-
2593
- render(App);
2594
- flushSync();
2595
-
2596
- const input = container.querySelector('input') as HTMLInputElement;
2597
- const div = container.querySelector('div') as HTMLDivElement;
2598
-
2599
- expect(input.value).toBe('');
2600
- expect(div.textContent).toBe('');
2601
-
2602
- input.value = 'abc';
2603
- input.dispatchEvent(new Event('input', { bubbles: true }));
2604
- flushSync();
2605
-
2606
- expect(input.value).toBe('ab');
2607
- expect(div.textContent).toBe('ab');
2608
- });
2609
-
2610
- it('should always prefer what getter returns even if setter mutates next', () => {
2611
- component App() {
2612
- let value = track(
2613
- '',
2614
- (val) => {
2615
- return val.replace(/[c,b]+/g, '');
2616
- },
2617
- (next) => {
2618
- if (next.includes('c')) {
2619
- next = next.replace(/c/g, '');
2620
- }
2621
- return next;
2622
- },
2623
- );
2624
-
2625
- <input type="text" {ref bindValue(value)} />
2626
- <div>{@value}</div>
2627
- }
2628
-
2629
- render(App);
2630
- flushSync();
2631
-
2632
- const input = container.querySelector('input') as HTMLInputElement;
2633
- const div = container.querySelector('div') as HTMLDivElement;
2634
-
2635
- expect(input.value).toBe('');
2636
- expect(div.textContent).toBe('');
2637
-
2638
- input.value = 'abc';
2639
- input.dispatchEvent(new Event('input', { bubbles: true }));
2640
- flushSync();
2641
-
2642
- expect(input.value).toBe('a');
2643
- expect(div.textContent).toBe('a');
2644
- });
2645
-
2646
- it(
2647
- 'should accurately reflect values mutated through an effect even after a setter mutation',
2648
- () => {
2649
- component App() {
2650
- let value = track(
2651
- '',
2652
- (val) => {
2653
- return val;
2654
- },
2655
- (next) => {
2656
- if (next.includes('c')) {
2657
- next = next.replace(/c/g, '');
2658
- }
2659
- return next;
2660
- },
2661
- );
2662
-
2663
- effect(() => {
2664
- @value;
2665
-
2666
- untrack(() => {
2667
- if (@value.includes('a')) {
2668
- @value = @value.replace(/a/g, '');
2669
- }
2670
- });
2671
- });
2672
- <input type="text" {ref bindValue(value)} />
2673
- <div>{@value}</div>
2674
- }
2675
-
2676
- render(App);
2677
- flushSync();
2678
-
2679
- const input = container.querySelector('input') as HTMLInputElement;
2680
- const div = container.querySelector('div') as HTMLDivElement;
2681
-
2682
- expect(input.value).toBe('');
2683
- expect(div.textContent).toBe('');
2684
-
2685
- input.value = 'abc';
2686
- input.dispatchEvent(new Event('input', { bubbles: true }));
2687
- flushSync();
2688
-
2689
- expect(input.value).toBe('b');
2690
- expect(div.textContent).toBe('b');
2691
- },
2692
- );
2693
-
2694
- it('should accurately reflect values mutated through a tracked setter via bind accessors', () => {
2695
- component App() {
2696
- let value = track('');
2697
- const value_accessors = [
2698
- () => {
2699
- return @value;
2700
- },
2701
- (v: string) => {
2702
- if (v.includes('c')) {
2703
- v = v.replace(/c/g, '');
2704
- }
2705
- @value = v;
2706
- },
2707
- ];
2708
-
2709
- <input type="text" {ref bindValue(...value_accessors)} />
2710
- }
2711
-
2712
- render(App);
2713
- flushSync();
2714
-
2715
- const input = container.querySelector('input') as HTMLInputElement;
2716
-
2717
- expect(input.value).toBe('');
2718
-
2719
- input.value = 'abc';
2720
- input.dispatchEvent(new Event('input', { bubbles: true }));
2721
- flushSync();
2722
-
2723
- expect(input.value).toBe('ab');
2724
- });
2725
-
2726
- it('should prefer what getter returns via bind accessors', () => {
2727
- component App() {
2728
- let value = track('');
2729
- const value_accessors = [
2730
- () => {
2731
- if (@value.includes('c')) {
2732
- return @value.replace(/c/g, '');
2733
- }
2734
- return @value;
2735
- },
2736
- (v: string) => {
2737
- @value = v;
2738
- },
2739
- ];
2740
-
2741
- <input type="text" {ref bindValue(...value_accessors)} />
2742
- }
2743
-
2744
- render(App);
2745
- flushSync();
2746
-
2747
- const input = container.querySelector('input') as HTMLInputElement;
2748
-
2749
- expect(input.value).toBe('');
2750
-
2751
- input.value = 'abc';
2752
- input.dispatchEvent(new Event('input', { bubbles: true }));
2753
- flushSync();
2754
-
2755
- expect(input.value).toBe('ab');
2756
- });
2757
-
2758
- it(
2759
- 'should always prefer what getter returns even if setter mutates next via bind accessors',
2760
- () => {
2761
- component App() {
2762
- let value = track('');
2763
- const value_accessors = [
2764
- () => {
2765
- return @value.replace(/[c,b]+/g, '');
2766
- },
2767
- (v: string) => {
2768
- if (v.includes('c')) {
2769
- v = v.replace(/c/g, '');
2770
- }
2771
- @value = v;
2772
- },
2773
- ];
2774
-
2775
- <input type="text" {ref bindValue(...value_accessors)} />
2776
- }
2777
-
2778
- render(App);
2779
- flushSync();
2780
-
2781
- const input = container.querySelector('input') as HTMLInputElement;
2782
-
2783
- expect(input.value).toBe('');
2784
-
2785
- input.value = 'abc';
2786
- input.dispatchEvent(new Event('input', { bubbles: true }));
2787
- flushSync();
2788
-
2789
- expect(input.value).toBe('a');
2790
- },
2791
- );
2792
-
2793
- it(
2794
- 'should accurately reflect values mutated through an effect even after a setter mutation via bind accessors',
2795
- () => {
2796
- component App() {
2797
- let value = track('');
2798
- const value_accessors = [
2799
- () => {
2800
- return @value;
2801
- },
2802
- (v: string) => {
2803
- if (v.includes('c')) {
2804
- v = v.replace(/c/g, '');
2805
- }
2806
- @value = v;
2807
- },
2808
- ];
2809
-
2810
- effect(() => {
2811
- @value;
2812
-
2813
- untrack(() => {
2814
- if (@value.includes('a')) {
2815
- @value = @value.replace(/a/g, '');
2816
- }
2817
- });
2818
- });
2819
- <input type="text" {ref bindValue(...value_accessors)} />
2820
- }
2821
-
2822
- render(App);
2823
- flushSync();
2824
-
2825
- const input = container.querySelector('input') as HTMLInputElement;
2826
-
2827
- expect(input.value).toBe('');
2828
-
2829
- input.value = 'abc';
2830
- input.dispatchEvent(new Event('input', { bubbles: true }));
2831
- flushSync();
2832
-
2833
- expect(input.value).toBe('b');
2834
- },
2835
- );
2836
2906
  });
2837
2907
 
2838
2908
  describe('bindFiles', () => {