From 92f373c17fb41cc8da6b2d14e1631f2be766f3ba Mon Sep 17 00:00:00 2001
From: Jungzl <13jungzl@gmail.com>
Date: Wed, 1 May 2024 10:51:40 +0800
Subject: [PATCH] fix(IndexBar): render active anchor correctly when passing
 sticky & stickyOffsetTop (#12837)

---
 packages/vant/src/index-bar/IndexBar.tsx      |  12 +-
 .../test/__snapshots__/index.spec.jsx.snap    | 383 ++++++++++++++++++
 .../vant/src/index-bar/test/index.spec.jsx    |  46 +++
 packages/vant/test/dom.ts                     |   8 +
 4 files changed, 447 insertions(+), 2 deletions(-)

diff --git a/packages/vant/src/index-bar/IndexBar.tsx b/packages/vant/src/index-bar/IndexBar.tsx
index 6441281d3..ab136180f 100644
--- a/packages/vant/src/index-bar/IndexBar.tsx
+++ b/packages/vant/src/index-bar/IndexBar.tsx
@@ -140,7 +140,11 @@ export default defineComponent({
         const match = getMatchAnchor(selectActiveIndex);
         if (match) {
           const rect = match.getRect(scrollParent.value, scrollParentRect);
-          active = getActiveAnchor(rect.top, rects);
+          if (props.sticky && props.stickyOffsetTop) {
+            active = getActiveAnchor(rect.top - props.stickyOffsetTop, rects);
+          } else {
+            active = getActiveAnchor(rect.top, rects);
+          }
         }
       } else {
         active = getActiveAnchor(scrollTop, rects);
@@ -229,7 +233,11 @@ export default defineComponent({
         }
 
         if (props.sticky && props.stickyOffsetTop) {
-          setRootScrollTop(getRootScrollTop() - props.stickyOffsetTop);
+          if (getRootScrollTop() === offsetHeight - scrollParentRect.height) {
+            setRootScrollTop(getRootScrollTop());
+          } else {
+            setRootScrollTop(getRootScrollTop() - props.stickyOffsetTop);
+          }
         }
 
         emit('select', match.index);
diff --git a/packages/vant/src/index-bar/test/__snapshots__/index.spec.jsx.snap b/packages/vant/src/index-bar/test/__snapshots__/index.spec.jsx.snap
index 7635b1f60..c90509c59 100644
--- a/packages/vant/src/index-bar/test/__snapshots__/index.spec.jsx.snap
+++ b/packages/vant/src/index-bar/test/__snapshots__/index.spec.jsx.snap
@@ -6,6 +6,389 @@ exports[`should allow to custom anchor content 1`] = `
 </div>
 `;
 
+exports[`should render active anchor when stick prop is true and has stickyOffsetTop 1`] = `
+<div class="van-index-bar">
+  <div class="van-index-bar__sidebar">
+    <span
+      class="van-index-bar__index"
+      data-index="A"
+    >
+      A
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="B"
+    >
+      B
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="C"
+    >
+      C
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="D"
+    >
+      D
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="E"
+    >
+      E
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="F"
+    >
+      F
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="G"
+    >
+      G
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="H"
+    >
+      H
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="I"
+    >
+      I
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="J"
+    >
+      J
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="K"
+    >
+      K
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="L"
+    >
+      L
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="M"
+    >
+      M
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="N"
+    >
+      N
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="O"
+    >
+      O
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="P"
+    >
+      P
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="Q"
+    >
+      Q
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="R"
+    >
+      R
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="S"
+    >
+      S
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="T"
+    >
+      T
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="U"
+    >
+      U
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="V"
+    >
+      V
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="W"
+    >
+      W
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="X"
+    >
+      X
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="Y"
+    >
+      Y
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="Z"
+    >
+      Z
+    </span>
+  </div>
+  <div data-index="0">
+    <div class="van-index-anchor">
+      A
+    </div>
+  </div>
+  <div style="height: 32px;">
+    A1
+  </div>
+  <div data-index="1">
+    <div class="van-index-anchor">
+      B
+    </div>
+  </div>
+  <div style="height: 32px;">
+    B1
+  </div>
+  <div data-index="2">
+    <div class="van-index-anchor">
+      C
+    </div>
+  </div>
+  <div style="height: 32px;">
+    C1
+  </div>
+</div>
+`;
+
+exports[`should render active anchor when stick prop is true and has stickyOffsetTop 2`] = `
+<div class="van-index-bar">
+  <div class="van-index-bar__sidebar">
+    <span
+      class="van-index-bar__index van-index-bar__index--active"
+      data-index="A"
+    >
+      A
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="B"
+    >
+      B
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="C"
+    >
+      C
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="D"
+    >
+      D
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="E"
+    >
+      E
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="F"
+    >
+      F
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="G"
+    >
+      G
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="H"
+    >
+      H
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="I"
+    >
+      I
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="J"
+    >
+      J
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="K"
+    >
+      K
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="L"
+    >
+      L
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="M"
+    >
+      M
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="N"
+    >
+      N
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="O"
+    >
+      O
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="P"
+    >
+      P
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="Q"
+    >
+      Q
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="R"
+    >
+      R
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="S"
+    >
+      S
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="T"
+    >
+      T
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="U"
+    >
+      U
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="V"
+    >
+      V
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="W"
+    >
+      W
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="X"
+    >
+      X
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="Y"
+    >
+      Y
+    </span>
+    <span
+      class="van-index-bar__index"
+      data-index="Z"
+    >
+      Z
+    </span>
+  </div>
+  <div
+    data-index="0"
+    style="height: 10px;"
+  >
+    <div
+      class="van-index-anchor van-index-anchor--sticky van-hairline--bottom"
+      style="transform: translate3d(0, 42px, 0);"
+    >
+      A
+    </div>
+  </div>
+  <div style="height: 32px;">
+    A1
+  </div>
+  <div data-index="1">
+    <div class="van-index-anchor">
+      B
+    </div>
+  </div>
+  <div style="height: 32px;">
+    B1
+  </div>
+  <div
+    data-index="2"
+    style
+  >
+    <div class="van-index-anchor">
+      C
+    </div>
+  </div>
+  <div style="height: 32px;">
+    C1
+  </div>
+</div>
+`;
+
 exports[`should update active anchor after page scroll 1`] = `
 <div class="van-index-bar">
   <div class="van-index-bar__sidebar">
diff --git a/packages/vant/src/index-bar/test/index.spec.jsx b/packages/vant/src/index-bar/test/index.spec.jsx
index 8554e7df0..b48f17b6e 100644
--- a/packages/vant/src/index-bar/test/index.spec.jsx
+++ b/packages/vant/src/index-bar/test/index.spec.jsx
@@ -3,6 +3,7 @@ import {
   mount,
   trigger,
   triggerDrag,
+  mockScrollTo,
   mockScrollTop,
   mockScrollIntoView,
 } from '../../../test';
@@ -206,3 +207,48 @@ test('should render teleport prop correctly', () => {
 
   expect(root.querySelector('.van-index-bar__sidebar')).toBeTruthy();
 });
+
+test('should render active anchor when stick prop is true and has stickyOffsetTop', async () => {
+  const nativeRect = Element.prototype.getBoundingClientRect;
+  Element.prototype.getBoundingClientRect = function () {
+    const { index } = this.dataset;
+    return {
+      top: index ? index * (10 + 32) : 0,
+      height: 10,
+    };
+  };
+
+  mockScrollTo();
+  const onSelect = vi.fn();
+  const onChange = vi.fn();
+
+  const wrapper = mount({
+    render() {
+      return (
+        <IndexBar onSelect={onSelect} stickyOffsetTop={42} onChange={onChange}>
+          <IndexAnchor index="A" data-index="0" />
+          <div style={{ height: '32px' }}>A1</div>
+          <IndexAnchor index="B" data-index="1" />
+          <div style={{ height: '32px' }}>B1</div>
+          <IndexAnchor index="C" data-index="2" />
+          <div style={{ height: '32px' }}>C1</div>
+        </IndexBar>
+      );
+    },
+  });
+
+  await nextTick();
+  expect(wrapper.html()).toMatchSnapshot();
+
+  const indexes = wrapper.findAll('.van-index-bar__index');
+  await indexes[0].trigger('click');
+  await trigger(window, 'scroll');
+
+  expect(wrapper.html()).toMatchSnapshot();
+  expect(onSelect).toHaveBeenCalledWith('A');
+  expect(onChange).toHaveBeenCalledWith('A');
+
+  wrapper.unmount();
+
+  Element.prototype.getBoundingClientRect = nativeRect;
+});
diff --git a/packages/vant/test/dom.ts b/packages/vant/test/dom.ts
index feb3e65d0..4337be427 100644
--- a/packages/vant/test/dom.ts
+++ b/packages/vant/test/dom.ts
@@ -37,6 +37,14 @@ function mockHTMLElementOffset() {
   });
 }
 
+export function mockScrollTo() {
+  const fn = vi.fn();
+  if (inBrowser) {
+    window.scrollTo = fn;
+  }
+  return fn;
+}
+
 export function mockScrollIntoView() {
   const fn = vi.fn();
   if (inBrowser) {