@crispy-seed/data-table 0.14.0 → 0.14.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/dist/index.cjs CHANGED
@@ -1668,6 +1668,11 @@ const ROOT_MARGIN = "160px";
1668
1668
  const elementRef = React.useRef(null);
1669
1669
  const [scrolledStart, setScrolledStart] = React.useState(false);
1670
1670
  const [scrolledEnd, setScrolledEnd] = React.useState(false);
1671
+ // The scroll container's own content-box width (0 until first measured). The
1672
+ // styled layer reads it to cap how much of a narrow viewport the sticky-left
1673
+ // pinned group may occupy (`useEffectiveColumns`) -- without a cap, a pinned
1674
+ // group wider than the container covers every scrolling column for good.
1675
+ const [viewportWidth, setViewportWidth] = React.useState(0);
1671
1676
  const [viewport, setViewport] = React.useState({
1672
1677
  scrollTop: 0,
1673
1678
  viewportHeight: 0
@@ -1688,6 +1693,7 @@ const ROOT_MARGIN = "160px";
1688
1693
  viewportHeight
1689
1694
  });
1690
1695
  }
1696
+ setViewportWidth(element.clientWidth);
1691
1697
  setScrolledStart(element.scrollLeft > 0);
1692
1698
  // A 1px slop: sub-pixel layout can leave `scrollLeft + clientWidth` a fraction
1693
1699
  // short of `scrollWidth` even when fully scrolled, which would otherwise leave
@@ -1697,16 +1703,16 @@ const ROOT_MARGIN = "160px";
1697
1703
  virtualized,
1698
1704
  rowHeight
1699
1705
  ]);
1700
- // The container can resize without scrolling (window resize, a bounded parent
1701
- // changing height) -- the viewport height feeds the window, so re-measure then too.
1706
+ // The container can resize without scrolling (window resize, rotating a phone, a
1707
+ // bounded parent changing height) -- the viewport height feeds the virtual window
1708
+ // and the width feeds the pinned-group cap, so re-measure then too.
1702
1709
  React.useEffect(()=>{
1703
1710
  const element = elementRef.current;
1704
- if (!virtualized || !element || typeof ResizeObserver === "undefined") return;
1711
+ if (!element || typeof ResizeObserver === "undefined") return;
1705
1712
  const observer = new ResizeObserver(()=>remeasure());
1706
1713
  observer.observe(element);
1707
1714
  return ()=>observer.disconnect();
1708
1715
  }, [
1709
- virtualized,
1710
1716
  remeasure
1711
1717
  ]);
1712
1718
  const scrollAreaRef = React.useCallback((node)=>{
@@ -1722,6 +1728,7 @@ const ROOT_MARGIN = "160px";
1722
1728
  scrollAreaRef,
1723
1729
  scrolledStart,
1724
1730
  scrolledEnd,
1731
+ viewportWidth,
1725
1732
  onScroll,
1726
1733
  remeasure,
1727
1734
  /** Row-virtualization state; `enabled: false` (the default) means render every row. */ virtual: {
@@ -3658,6 +3665,13 @@ function RatingCell({ value, maxRating = 5 }) {
3658
3665
  });
3659
3666
  }
3660
3667
 
3668
+ /** Share of the scroll container's width the sticky-left pinned group may occupy
3669
+ * before FURTHER pinned columns stop sticking and scroll like any other column. A
3670
+ * pinned group at or past the container's width pins every pixel of it: the
3671
+ * scrolling columns slide in underneath the sticky cells and can never be seen (a
3672
+ * 375px phone with Serial + one more pinned column showed nothing else, ever). The
3673
+ * FIRST pinned column always sticks regardless -- the entity/name column staying
3674
+ * frozen is the whole point of pinning. */ const MAX_PINNED_VIEWPORT_SHARE = 0.6;
3661
3675
  /** Fallback width (px) used to compute a pinned column's cumulative sticky offset when
3662
3676
  * it has no explicit `width` and hasn't been drag-resized -- pinning needs a STATIC
3663
3677
  * number to put in an inline `insetInlineStart`, and `table-layout: fixed` only
@@ -3677,7 +3691,7 @@ function RatingCell({ value, maxRating = 5 }) {
3677
3691
  * one and a header row built from the other drift out of sync under `table-layout:
3678
3692
  * fixed`'s own column-to-cell matching.
3679
3693
  */ function useEffectiveColumns(columns, options) {
3680
- const { visibleColumnOrder, getColumnLabel, getColumnWidth, pinnedColumnKeys } = useDataTableContext();
3694
+ const { visibleColumnOrder, getColumnLabel, getColumnWidth, pinnedColumnKeys, viewportWidth } = useDataTableContext();
3681
3695
  const { selectable, stickyFirst } = options;
3682
3696
  return React__namespace.useMemo(()=>{
3683
3697
  const byKey = new Map(columns.map((column)=>[
@@ -3703,11 +3717,14 @@ function RatingCell({ value, maxRating = 5 }) {
3703
3717
  const bPinned = pinnedSet.has(b) ? 0 : 1;
3704
3718
  return aPinned - bPinned;
3705
3719
  });
3720
+ // 0 = not measured yet (first paint / jsdom) -- no cap rather than a guess.
3721
+ const pinBudget = viewportWidth ? viewportWidth * MAX_PINNED_VIEWPORT_SHARE : Number.POSITIVE_INFINITY;
3706
3722
  let cursor = selectable ? 40 : 0;
3707
3723
  let pinnedCount = 0;
3724
+ let budgetSpent = false;
3708
3725
  const entries = ordered.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>{
3709
3726
  const rawWidth = getColumnWidth(column.key) ?? column.width;
3710
- const pinned = pinnedSet.has(column.key);
3727
+ const inPinnedSet = pinnedSet.has(column.key);
3711
3728
  // Fix (0.10.4, screenshot-confirmed pinned-column overlay): a pinned column
3712
3729
  // with no explicit/resized width used to keep `width` as `undefined` here,
3713
3730
  // which `DataTableColGroup` (DataTable.tsx) renders as an AUTO `<col>` --
@@ -3727,7 +3744,15 @@ function RatingCell({ value, maxRating = 5 }) {
3727
3744
  // without its own width now always renders at exactly
3728
3745
  // `DEFAULT_PINNED_COLUMN_WIDTH`, matching what every other pinned column's
3729
3746
  // offset already assumed it would be.
3730
- const width = pinned && rawWidth === undefined ? DEFAULT_PINNED_COLUMN_WIDTH : rawWidth;
3747
+ const width = inPinnedSet && rawWidth === undefined ? DEFAULT_PINNED_COLUMN_WIDTH : rawWidth;
3748
+ // Sticks only while the group still fits the pin budget (see
3749
+ // MAX_PINNED_VIEWPORT_SHARE); the first pinned column always sticks.
3750
+ // Once one pinned column overflows, every later one scrolls too -- a narrower
3751
+ // late column must not stick past a gap.
3752
+ if (inPinnedSet && pinnedCount > 0 && cursor + (width ?? DEFAULT_PINNED_COLUMN_WIDTH) > pinBudget) {
3753
+ budgetSpent = true;
3754
+ }
3755
+ const pinned = inPinnedSet && !budgetSpent;
3731
3756
  const pinOffset = pinned ? cursor : 0;
3732
3757
  if (pinned) {
3733
3758
  cursor += width ?? DEFAULT_PINNED_COLUMN_WIDTH;
@@ -3766,7 +3791,8 @@ function RatingCell({ value, maxRating = 5 }) {
3766
3791
  getColumnWidth,
3767
3792
  pinnedColumnKeys,
3768
3793
  selectable,
3769
- stickyFirst
3794
+ stickyFirst,
3795
+ viewportWidth
3770
3796
  ]);
3771
3797
  }
3772
3798
  /** The 40px selection column's own sticky-left state -- `stickyFirst` (the table-wide
package/dist/index.js CHANGED
@@ -1649,6 +1649,11 @@ const ROOT_MARGIN = "160px";
1649
1649
  const elementRef = useRef(null);
1650
1650
  const [scrolledStart, setScrolledStart] = useState(false);
1651
1651
  const [scrolledEnd, setScrolledEnd] = useState(false);
1652
+ // The scroll container's own content-box width (0 until first measured). The
1653
+ // styled layer reads it to cap how much of a narrow viewport the sticky-left
1654
+ // pinned group may occupy (`useEffectiveColumns`) -- without a cap, a pinned
1655
+ // group wider than the container covers every scrolling column for good.
1656
+ const [viewportWidth, setViewportWidth] = useState(0);
1652
1657
  const [viewport, setViewport] = useState({
1653
1658
  scrollTop: 0,
1654
1659
  viewportHeight: 0
@@ -1669,6 +1674,7 @@ const ROOT_MARGIN = "160px";
1669
1674
  viewportHeight
1670
1675
  });
1671
1676
  }
1677
+ setViewportWidth(element.clientWidth);
1672
1678
  setScrolledStart(element.scrollLeft > 0);
1673
1679
  // A 1px slop: sub-pixel layout can leave `scrollLeft + clientWidth` a fraction
1674
1680
  // short of `scrollWidth` even when fully scrolled, which would otherwise leave
@@ -1678,16 +1684,16 @@ const ROOT_MARGIN = "160px";
1678
1684
  virtualized,
1679
1685
  rowHeight
1680
1686
  ]);
1681
- // The container can resize without scrolling (window resize, a bounded parent
1682
- // changing height) -- the viewport height feeds the window, so re-measure then too.
1687
+ // The container can resize without scrolling (window resize, rotating a phone, a
1688
+ // bounded parent changing height) -- the viewport height feeds the virtual window
1689
+ // and the width feeds the pinned-group cap, so re-measure then too.
1683
1690
  useEffect(()=>{
1684
1691
  const element = elementRef.current;
1685
- if (!virtualized || !element || typeof ResizeObserver === "undefined") return;
1692
+ if (!element || typeof ResizeObserver === "undefined") return;
1686
1693
  const observer = new ResizeObserver(()=>remeasure());
1687
1694
  observer.observe(element);
1688
1695
  return ()=>observer.disconnect();
1689
1696
  }, [
1690
- virtualized,
1691
1697
  remeasure
1692
1698
  ]);
1693
1699
  const scrollAreaRef = useCallback((node)=>{
@@ -1703,6 +1709,7 @@ const ROOT_MARGIN = "160px";
1703
1709
  scrollAreaRef,
1704
1710
  scrolledStart,
1705
1711
  scrolledEnd,
1712
+ viewportWidth,
1706
1713
  onScroll,
1707
1714
  remeasure,
1708
1715
  /** Row-virtualization state; `enabled: false` (the default) means render every row. */ virtual: {
@@ -3639,6 +3646,13 @@ function RatingCell({ value, maxRating = 5 }) {
3639
3646
  });
3640
3647
  }
3641
3648
 
3649
+ /** Share of the scroll container's width the sticky-left pinned group may occupy
3650
+ * before FURTHER pinned columns stop sticking and scroll like any other column. A
3651
+ * pinned group at or past the container's width pins every pixel of it: the
3652
+ * scrolling columns slide in underneath the sticky cells and can never be seen (a
3653
+ * 375px phone with Serial + one more pinned column showed nothing else, ever). The
3654
+ * FIRST pinned column always sticks regardless -- the entity/name column staying
3655
+ * frozen is the whole point of pinning. */ const MAX_PINNED_VIEWPORT_SHARE = 0.6;
3642
3656
  /** Fallback width (px) used to compute a pinned column's cumulative sticky offset when
3643
3657
  * it has no explicit `width` and hasn't been drag-resized -- pinning needs a STATIC
3644
3658
  * number to put in an inline `insetInlineStart`, and `table-layout: fixed` only
@@ -3658,7 +3672,7 @@ function RatingCell({ value, maxRating = 5 }) {
3658
3672
  * one and a header row built from the other drift out of sync under `table-layout:
3659
3673
  * fixed`'s own column-to-cell matching.
3660
3674
  */ function useEffectiveColumns(columns, options) {
3661
- const { visibleColumnOrder, getColumnLabel, getColumnWidth, pinnedColumnKeys } = useDataTableContext();
3675
+ const { visibleColumnOrder, getColumnLabel, getColumnWidth, pinnedColumnKeys, viewportWidth } = useDataTableContext();
3662
3676
  const { selectable, stickyFirst } = options;
3663
3677
  return React.useMemo(()=>{
3664
3678
  const byKey = new Map(columns.map((column)=>[
@@ -3684,11 +3698,14 @@ function RatingCell({ value, maxRating = 5 }) {
3684
3698
  const bPinned = pinnedSet.has(b) ? 0 : 1;
3685
3699
  return aPinned - bPinned;
3686
3700
  });
3701
+ // 0 = not measured yet (first paint / jsdom) -- no cap rather than a guess.
3702
+ const pinBudget = viewportWidth ? viewportWidth * MAX_PINNED_VIEWPORT_SHARE : Number.POSITIVE_INFINITY;
3687
3703
  let cursor = selectable ? 40 : 0;
3688
3704
  let pinnedCount = 0;
3705
+ let budgetSpent = false;
3689
3706
  const entries = ordered.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>{
3690
3707
  const rawWidth = getColumnWidth(column.key) ?? column.width;
3691
- const pinned = pinnedSet.has(column.key);
3708
+ const inPinnedSet = pinnedSet.has(column.key);
3692
3709
  // Fix (0.10.4, screenshot-confirmed pinned-column overlay): a pinned column
3693
3710
  // with no explicit/resized width used to keep `width` as `undefined` here,
3694
3711
  // which `DataTableColGroup` (DataTable.tsx) renders as an AUTO `<col>` --
@@ -3708,7 +3725,15 @@ function RatingCell({ value, maxRating = 5 }) {
3708
3725
  // without its own width now always renders at exactly
3709
3726
  // `DEFAULT_PINNED_COLUMN_WIDTH`, matching what every other pinned column's
3710
3727
  // offset already assumed it would be.
3711
- const width = pinned && rawWidth === undefined ? DEFAULT_PINNED_COLUMN_WIDTH : rawWidth;
3728
+ const width = inPinnedSet && rawWidth === undefined ? DEFAULT_PINNED_COLUMN_WIDTH : rawWidth;
3729
+ // Sticks only while the group still fits the pin budget (see
3730
+ // MAX_PINNED_VIEWPORT_SHARE); the first pinned column always sticks.
3731
+ // Once one pinned column overflows, every later one scrolls too -- a narrower
3732
+ // late column must not stick past a gap.
3733
+ if (inPinnedSet && pinnedCount > 0 && cursor + (width ?? DEFAULT_PINNED_COLUMN_WIDTH) > pinBudget) {
3734
+ budgetSpent = true;
3735
+ }
3736
+ const pinned = inPinnedSet && !budgetSpent;
3712
3737
  const pinOffset = pinned ? cursor : 0;
3713
3738
  if (pinned) {
3714
3739
  cursor += width ?? DEFAULT_PINNED_COLUMN_WIDTH;
@@ -3747,7 +3772,8 @@ function RatingCell({ value, maxRating = 5 }) {
3747
3772
  getColumnWidth,
3748
3773
  pinnedColumnKeys,
3749
3774
  selectable,
3750
- stickyFirst
3775
+ stickyFirst,
3776
+ viewportWidth
3751
3777
  ]);
3752
3778
  }
3753
3779
  /** The 40px selection column's own sticky-left state -- `stickyFirst` (the table-wide
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crispy-seed/data-table",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Self-contained, publishable build of seed-design's Attio-measured DataTable, ResizablePanel and RecordPreview — one install, one CSS import.",
5
5
  "repository": {
6
6
  "type": "git",