Skip to content

fix: SwipeAction swiped#1523

Open
rayhomie wants to merge 1 commit intomasterfrom
fix-swipe-action-swiped
Open

fix: SwipeAction swiped#1523
rayhomie wants to merge 1 commit intomasterfrom
fix-swipe-action-swiped

Conversation

@rayhomie
Copy link
Collaborator

@rayhomie rayhomie commented Dec 17, 2025

Summary by CodeRabbit

发版说明

  • 新功能
    • 新增默认滑动状态配置,支持向左或向右的默认滑动展示。
    • 新增受控滑动功能,允许通过 UI 按钮程序化控制滑动状态。
    • 增强了滑动状态的初始化流程,改进了受控和非受控滑动的处理机制。

✏️ Tip: You can customize this high-level summary in your review settings.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Dec 17, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Dec 17, 2025

😭 Deploy PR Preview 584c9be failed. Build logs

🤖 By surge-preview

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

📝 Walkthrough

详细说明

此PR为SwipeAction组件引入受控刷卡功能,包括演示页面的新样式布局和组件源码中的初始化逻辑改进,支持defaultSwiped和受控swiped状态模式。

变更

组件 / 文件 变更摘要
演示页面更新
demo/pages/SwipeAction/index.axml, demo/pages/SwipeAction/index.less, demo/pages/SwipeAction/index.ts
添加两个新的刷卡演示配置(defaultSwiped="right"和defaultSwiped="left"),引入受控刷卡演示及其对应的UI控制面板;新增.control-buttons样式以支持水平按钮布局;添加controlledSwipe数据字段和onControlSwipe()方法处理UI控制
核心组件逻辑改进
src/SwipeAction/index.ts
优化刷卡状态处理,实现受控vs非受控模式优先级(swiped优先于defaultSwiped);改进初始化流程为两步式(先计算按钮宽度,后延迟初始化刷卡位置);完善didUpdate中的状态变更处理,使用swipedChanged标志区分swiped变化和elasticity/damping变化,明确定义各分支的重置逻辑

代码审查工作量

🎯 3 (Moderate) | ⏱️ ~20 分钟

需要重点关注的区域:

  • src/SwipeAction/index.ts 中的受控vs非受控初始化流程,包括setButtonItemWidth和50ms延迟的两步初始化逻辑
  • didUpdate中swipedChanged标志的状态分支处理,确保swiped变化、elasticity/damping变化等场景的重置逻辑正确性
  • 初始刷卡位置的宽度依赖计算,验证timeout机制的可靠性

可能相关的PR

  • Fix swipe action #1516:同样修改src/SwipeAction/index.ts并涉及swipedL/swipedR状态重置逻辑,与本PR的swipe状态管理改进相关

建议标签

bug

诗歌

🐰 按钮宽度需先量,
再将姿态缓缓放,
受控非受各有序,
刷卡之舞更流畅!

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive 标题 'fix: SwipeAction swiped' 过于简洁且缺乏具体信息,没有清晰说明本次变更的核心内容。虽然涉及 SwipeAction 组件,但 'swiped' 过于宽泛,无法准确反映添加受控滑动、默认滑动状态和 UI 控制面板等具体改动。 建议优化标题为更具体的描述,例如 'feat: Add controlled and default swiped state support to SwipeAction' 或 'feat: SwipeAction controlled swiped state with UI controls',以清晰传达核心功能增强。
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-swipe-action-swiped

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @rayhomie, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the SwipeAction component by introducing a controlled swiped property. This new feature allows for external management of the swipe action's state, providing greater flexibility for user interfaces that require dynamic control over component behavior. The changes ensure proper initialization and updates of the swipe state, along with clear examples in the demo to illustrate its usage.

Highlights

  • Controlled SwipeAction: Introduced a new swiped property to the SwipeAction component, enabling programmatic control over its open/closed state. This allows developers to dynamically open or close the swipe action from parent components.
  • Default Swiped State: The SwipeAction component now prioritizes the new swiped property for initial state, falling back to defaultSwiped if swiped is not provided. This ensures a clear hierarchy for setting the initial swipe direction.
  • Lifecycle Management: Updated the didMount and didUpdate lifecycle methods to correctly handle the swiped property. This includes logic to open or close the swipe action based on swiped value changes and incorporates setTimeout calls to ensure button widths are calculated before applying swipe transformations.
  • Demo Enhancements: Added new examples to the SwipeAction demo page, showcasing both the defaultSwiped property for initial expansion and the new controlled swiped property with interactive buttons to demonstrate dynamic control.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added the bug Something isn't working label Dec 17, 2025
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the SwipeAction component by adding support for both controlled (swiped prop) and uncontrolled (defaultSwiped prop) modes. The implementation correctly handles the component's lifecycle to reflect the swiped state. My review focuses on improving the robustness of the asynchronous rendering logic and enhancing type safety, which will make the component more reliable and maintainable.

Comment on lines +109 to +120
setTimeout(() => {
this.initWidth((maxSwipe: any) => {
if (maxSwipe) {
const direction = initialSwiped === 'left' ? 'left' : 'right';
this.setData({
swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
swipedR: direction === 'right',
swipedL: direction === 'left',
});
}
});
}, 50);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The use of a fixed setTimeout(..., 50) to wait for component rendering before calculating widths is fragile and can lead to race conditions on slower devices or under heavy load. This could result in the swipe action not initializing to the correct defaultSwiped state. Please consider a more robust approach, such as using my.nextTick (if available on your target platform) or another mechanism that guarantees execution after the render cycle is complete.

Comment on lines +144 to +157
setTimeout(() => {
this.initWidth((maxSwipe: any) => {
if (maxSwipe) {
const direction = swiped === 'left' ? 'left' : 'right';
this.setData({
swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
swipedR: direction === 'right',
swipedL: direction === 'left',
tapTypeL: '',
tapTypeR: '',
});
}
});
}, 50);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the implementation in didMount, using a fixed setTimeout(..., 50) here is fragile and can lead to race conditions when the swiped prop is updated. This could cause the component to not open correctly. A more robust post-render callback mechanism like my.nextTick should be preferred to ensure reliability across different devices and load conditions.

if (initialSwiped) {
// 需要等待按钮宽度设置完成后再初始化展开状态
setTimeout(() => {
this.initWidth((maxSwipe: any) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The maxSwipe parameter is typed as any. For better type safety and code clarity, it should be explicitly typed as number, since it represents a width value obtained from initWidth.

Suggested change
this.initWidth((maxSwipe: any) => {
this.initWidth((maxSwipe: number) => {

// swiped 变为 true/'left'/'right',需要打开
// 需要等待下一个事件循环,确保按钮宽度已经渲染
setTimeout(() => {
this.initWidth((maxSwipe: any) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The maxSwipe parameter is typed as any. It should be typed as number to improve type safety and make the code easier to understand, as it represents a width value.

Suggested change
this.initWidth((maxSwipe: any) => {
this.initWidth((maxSwipe: number) => {

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/SwipeAction/index.ts (1)

123-173: 定时器清理缺失和条件逻辑需要验证

  1. Line 144 的 setTimeout 同样缺少清理机制。didMount (Line 109) 和 didUpdate (Line 144) 中的定时器都未保存到全局的 myTimeOut 变量,且组件中不存在 detached 生命周期方法来清理这些定时器。当组件卸载时,如果 50ms 的定时器仍在挂起状态,会导致内存泄漏。建议在定时器分配时保存 ID 并在组件卸载时清除。

  2. Lines 131-172 的条件分支逻辑确实存在问题。当 swipedChangedtrue 时,会进入 lines 132-158 的分支,导致 lines 159-167 的 else if 分支被跳过。这意味着如果 swipedelasticity 同时变化,elasticity 导致的状态重置 (Lines 161-167) 会被意外跳过,虽然 Line 171 的 inertiaWidth 更新仍会执行。请确认这是否为预期设计,还是应该在 swipedChanged 分支中也处理 elasticity 变化。

🧹 Nitpick comments (2)
src/SwipeAction/index.ts (2)

109-109: 建议使用常量替代魔法数字

50ms 的延迟时间在代码中出现两次 (Lines 109, 144, 以及 Line 120 和 157),应该提取为具名常量以提高可读性和可维护性。

+const SWIPE_INIT_DELAY = 50; // 等待按钮宽度计算完成的延迟时间
+
 Component(
   transformOptions({
     // ...
     didMount() {
       // ...
       if (initialSwiped) {
         setTimeout(() => {
           // ...
-        }, 50);
+        }, SWIPE_INIT_DELAY);
       }
     },
     didUpdate(prevProp) {
       // ...
       if (swipedChanged) {
         // ...
         } else {
           setTimeout(() => {
             // ...
-          }, 50);
+          }, SWIPE_INIT_DELAY);
         }
       }
     },

Also applies to: 144-144


110-119: 建议添加错误处理

initWidth 的回调中,如果 maxSwipe 为假值(undefinednull0 等),当前代码会静默失败。虽然有 if (maxSwipe) 检查,但建议添加日志或警告,帮助开发者排查按钮宽度计算失败的问题。

 this.initWidth((maxSwipe: any) => {
   if (maxSwipe) {
     const direction = swiped === 'left' ? 'left' : 'right';
     this.setData({
       swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
       swipedR: direction === 'right',
       swipedL: direction === 'left',
       tapTypeL: '',
       tapTypeR: '',
     });
+  } else {
+    console.warn('[SwipeAction] 无法获取按钮宽度,初始化滑动状态失败');
   }
 });

Also applies to: 145-156

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08a3cb9 and 584c9be.

📒 Files selected for processing (4)
  • demo/pages/SwipeAction/index.axml (1 hunks)
  • demo/pages/SwipeAction/index.less (1 hunks)
  • demo/pages/SwipeAction/index.ts (2 hunks)
  • src/SwipeAction/index.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: preview
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (4)
demo/pages/SwipeAction/index.less (1)

62-71: 样式实现合理

为演示用例添加的控制按钮样式结构清晰,使用了项目现有的 LESS 变量和主题系统,实现符合预期。

demo/pages/SwipeAction/index.ts (2)

3-3: 演示代码实现正确

添加 controlledSwipe 数据属性用于演示受控模式,实现简洁明了。


103-108: 事件处理逻辑清晰

onControlSwipe 方法正确地从事件对象中读取方向参数并更新状态,符合小程序的事件处理模式。

demo/pages/SwipeAction/index.axml (1)

333-382: 演示用例完整

新增的演示部分清晰地展示了 defaultSwiped 非受控模式和 swiped 受控模式的用法,有助于开发者理解两种使用模式的区别。

Comment on lines 97 to 122
didMount() {
const { defaultSwiped, elasticity } = this.getProps();
this.setButtonItemWidth();
const { defaultSwiped, swiped, elasticity } = this.getProps();
this.setData({ inertiaWidth: !isOldVersion && elasticity ? 20 : 0 });
if (defaultSwiped) {
this.initWidth((maxSwipe: any) => {
maxSwipe &&
this.setData({
swipeX: (maxSwipe + 0.01) * (defaultSwiped === 'right' ? -1 : 1),
swipedR: defaultSwiped === 'right',
swipedL: defaultSwiped === 'left',
});
});

// 优先使用 swiped 属性(受控),其次使用 defaultSwiped(非受控)
const initialSwiped = swiped || defaultSwiped;

// 先设置按钮宽度,然后在回调中处理初始展开状态
this.setButtonItemWidth();

if (initialSwiped) {
// 需要等待按钮宽度设置完成后再初始化展开状态
setTimeout(() => {
this.initWidth((maxSwipe: any) => {
if (maxSwipe) {
const direction = initialSwiped === 'left' ? 'left' : 'right';
this.setData({
swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
swipedR: direction === 'right',
swipedL: direction === 'left',
});
}
});
}, 50);
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

缺少定时器清理可能导致状态更新异常

didMount 中的 setTimeout (Line 109) 没有保存返回的 timer ID,也没有在组件卸载或更新时清理。如果组件在 50ms 内被销毁或 swiped 属性再次变化,可能导致过期的状态更新。

建议将 timer ID 保存到实例变量,并在 didUnmount 和相关更新逻辑中清理:

+let initTimer = null;
+
 Component(
   transformOptions({
     props: SwipeActionDefaultProps,
     didMount() {
       const { defaultSwiped, swiped, elasticity } = this.getProps();
       this.setData({ inertiaWidth: !isOldVersion && elasticity ? 20 : 0 });
       
       const initialSwiped = swiped || defaultSwiped;
       this.setButtonItemWidth();
       
       if (initialSwiped) {
-        setTimeout(() => {
+        initTimer = setTimeout(() => {
           this.initWidth((maxSwipe: any) => {
             if (maxSwipe) {
               const direction = initialSwiped === 'left' ? 'left' : 'right';
               this.setData({
                 swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
                 swipedR: direction === 'right',
                 swipedL: direction === 'left',
               });
             }
           });
         }, 50);
       }
     },
+    didUnmount() {
+      if (initTimer) {
+        clearTimeout(initTimer);
+        initTimer = null;
+      }
+    },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/SwipeAction/index.ts around lines 97 to 122, the setTimeout started at
line 109 isn’t storing its timer ID and therefore isn’t cleared on unmount or
when props change; save the timeout ID to an instance property (e.g.,
this._initTimeout), clear it with clearTimeout(this._initTimeout) and null it in
didUnmount, and also clear any existing timeout before creating a new one when
reacting to prop updates (like swiped changes) to prevent stale callbacks from
updating state after the component is destroyed or reconfigured.

Comment on lines +110 to +119
this.initWidth((maxSwipe: any) => {
if (maxSwipe) {
const direction = initialSwiped === 'left' ? 'left' : 'right';
this.setData({
swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
swipedR: direction === 'right',
swipedL: direction === 'left',
});
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

重复的初始化逻辑应该提取为独立方法

Lines 110-119 (在 didMount 中) 和 Lines 145-156 (在 didUpdate 中) 包含几乎完全相同的初始化逻辑。这违反了 DRY 原则,增加了维护成本和出错风险。

建议提取为独立的辅助方法:

+    applySwipedState(swiped, delay = 50) {
+      setTimeout(() => {
+        this.initWidth((maxSwipe: any) => {
+          if (maxSwipe) {
+            const direction = swiped === 'left' ? 'left' : 'right';
+            this.setData({
+              swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
+              swipedR: direction === 'right',
+              swipedL: direction === 'left',
+              tapTypeL: '',
+              tapTypeR: '',
+            });
+          }
+        });
+      }, delay);
+    },
     didMount() {
       const { defaultSwiped, swiped, elasticity } = this.getProps();
       this.setData({ inertiaWidth: !isOldVersion && elasticity ? 20 : 0 });
       
       const initialSwiped = swiped || defaultSwiped;
       this.setButtonItemWidth();
       
       if (initialSwiped) {
-        setTimeout(() => {
-          this.initWidth((maxSwipe: any) => {
-            if (maxSwipe) {
-              const direction = initialSwiped === 'left' ? 'left' : 'right';
-              this.setData({
-                swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
-                swipedR: direction === 'right',
-                swipedL: direction === 'left',
-              });
-            }
-          });
-        }, 50);
+        this.applySwipedState(initialSwiped);
       }
     },
     didUpdate(prevProp) {
       // ... 省略其他代码
       if (swipedChanged) {
         if (!swiped) {
           // ... 重置逻辑
         } else {
-          setTimeout(() => {
-            this.initWidth((maxSwipe: any) => {
-              if (maxSwipe) {
-                const direction = swiped === 'left' ? 'left' : 'right';
-                this.setData({
-                  swipeX: (maxSwipe + 0.01) * (direction === 'right' ? -1 : 1),
-                  swipedR: direction === 'right',
-                  swipedL: direction === 'left',
-                  tapTypeL: '',
-                  tapTypeR: '',
-                });
-              }
-            });
-          }, 50);
+          this.applySwipedState(swiped);
         }
       }
     },

Also applies to: 145-156

🤖 Prompt for AI Agents
In src/SwipeAction/index.ts around lines 110-119 and 145-156, the same
initialization code that reads maxSwipe and sets swipeX, swipedR, swipedL is
duplicated; extract that logic into a single private/helper method (e.g.,
initSwipeState(initialSwiped?: string)) that calls this.initWidth and in its
callback computes direction, swipeX and sets the data, then replace the
duplicated blocks in didMount and didUpdate with calls to this new helper
(ensure correct this binding and preserve the same behavior when initialSwiped
is undefined).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant