feat(gestures): Add propagate prop for tap event propagation control (#2277)#3539
feat(gestures): Add propagate prop for tap event propagation control (#2277)#3539mattgperry merged 2 commits intomainfrom
Conversation
Code Review for PR #3539: Add stopTapPropagation propSummaryThis PR adds a ✅ Strengths1. Elegant Solution Design
2. Excellent Test Coverage
3. Minimal API Surface
4. Proper Integration
🔍 Observations & Minor Considerations1. Event Lifecycle & WeakSet Behavior
2. Interaction with
Suggestion: Add a test case combining 3. Performance Consideration
4. Documentation Completeness
5. Suggestion: Add a test verifying that 🐛 Potential Issues1. Missing Type Export Assessment: Likely intentional. Only mention if this becomes a user request. 2. React Event System Compatibility ✅
🔒 SecurityNo security concerns identified. The feature:
⚡ PerformanceThe implementation is efficient:
Minor optimization opportunity: The WeakSet check could potentially be moved even earlier (before 📋 RecommendationsHigh Priority:
Low Priority: Code snippet for additional test: test("stopTapPropagation prevents parent onTapCancel from firing", async () => {
const parentTapCancel = jest.fn()
const childTapCancel = jest.fn()
const Component = () => (
<motion.div onTapCancel={() => parentTapCancel()}>
<motion.div
data-testid="child"
onTapCancel={() => childTapCancel()}
stopTapPropagation
/>
</motion.div>
)
const { getByTestId, rerender } = render(<Component />)
rerender(<Component />)
pointerDown(getByTestId("child"))
// Simulate a cancel by moving pointer away
pointerCancel(getByTestId("child"))
await nextFrame()
expect(childTapCancel).toBeCalledTimes(1)
expect(parentTapCancel).toBeCalledTimes(0)
})✨ Overall AssessmentVerdict: Approve with minor suggestions ✅ This is a well-designed, well-tested feature that solves a real user pain point (issue #2277). The implementation is elegant and follows the repository's patterns and conventions. The test coverage is comprehensive, and the code quality is high. The suggestions above are optional improvements that would make the feature even more robust, but the current implementation is production-ready. Great work! 🎉 |
…dlers from firing Fixes #2277. The documented workaround using onPointerDownCapture + stopPropagation is broken in React 17+ because React intercepts native events at the root. This adds a Motion-native mechanism using a WeakSet to track claimed pointerdown events, allowing child elements to prevent ancestor tap gesture handlers without affecting other listeners. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
a958583 to
ba85e4c
Compare
PR Review: feat(gestures): Add stopTapPropagation prop (#2277)SummaryThis PR adds a ✅ Strengths1. Excellent Architecture DecisionThe use of
2. Comprehensive Test CoverageThe four new tests cover all critical scenarios:
All tests follow the project's established patterns (using 3. Minimal API SurfaceThe prop is boolean and opt-in, maintaining backward compatibility. The naming 4. Clean Integration
🔍 Observations & Minor Considerations1. Early Return PositionIn packages/motion-dom/src/gestures/press/index.ts:61, the check happens before adding to 2. Keyboard AccessibilityThe press gesture also supports keyboard events (Enter key). The Question for author: Should keyboard-triggered taps also respect 🔒 SecurityNo security concerns identified - event handling follows browser security model. 🚀 PerformanceExcellent: WeakSet lookup is O(1), and the early return pattern means minimal overhead when the prop isn't used. ✨ Overall AssessmentThis is a high-quality PR that:
The WeakSet approach is particularly clever—it coordinates between Motion components without touching the native event system. Recommendation: ✅ ApproveMinor suggestion: Document whether keyboard-triggered taps should also respect Great work! This fixes a long-standing issue elegantly. |
…lse }} Avoids double negative naming and makes the API extensible for other gesture types (hover, drag, etc.). Also adds Playwright E2E tests verifying the stopPropagation option works at the motion-dom press() level. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PR Review: Tap Event Propagation ControlI've completed a thorough review of this PR. Overall, this is excellent work that solves a real problem (#2277) in an elegant, maintainable way. The implementation aligns perfectly with the codebase's existing patterns. ✅ Strengths1. Architecture & Design
2. Code Quality
3. Test CoverageComprehensive testing across both Jest and Playwright:
🔍 Observations & Suggestions1. Memory Management ✅The WeakSet automatically garbage collects when events are no longer referenced - no manual cleanup needed. This aligns with existing gesture patterns ( No action needed - implementation is correct. 2. Edge Case: Event ReuseWhile highly unlikely in practice, if a browser reuses Risk assessment: Negligible - browsers create new event objects per interaction. The WeakSet's lifecycle is tied to event object lifetime, which is appropriate here. 3. Cross-Frame ScenariosIf events bubble across iframe boundaries, the module-level WeakSet won't track cross-frame events. Impact: Minimal - cross-frame gesture handling is already complex and typically requires explicit coordination. Current implementation correctly scopes to single-context scenarios. 4. Documentation ClarityThe JSDoc at packages/motion-dom/src/node/types.ts:1069-1078 is excellent. Consider also mentioning in the PR description or docs that this doesn't affect:
5. Performance ✅
🎯 Specific Code Reviewpackages/motion-dom/src/gestures/press/index.ts// Line 61: Early exit is performant ✅
if (claimedPointerDownEvents.has(startEvent)) return
// Lines 65-67: Clean and minimal ✅
if (options.stopPropagation) {
claimedPointerDownEvents.add(startEvent)
}Suggestion: Consider adding a brief comment explaining why we check WeakSet before the isPressing check: // Skip if another Motion element already claimed this pointer event
if (claimedPointerDownEvents.has(startEvent)) returnpackages/framer-motion/src/gestures/press.ts// Lines 49-52: Proper feature integration ✅
{
useGlobalTarget: globalTapTarget,
stopPropagation: propagate?.tap === false,
}Clean optional chaining usage aligns with codebase style guidelines. 🔒 Security ✅No security concerns. The implementation:
🧪 Test Suggestions (Optional Enhancements)While existing tests are comprehensive, consider adding:
These are nice-to-haves rather than blockers. 📊 Bundle Size ImpactGiven the emphasis on file size in CLAUDE.md, the additions are minimal:
Estimate: <100 bytes added to production bundle - excellent cost/benefit ratio. ✅ Verdict: ApprovedThis PR is production-ready. The implementation is:
The API design ( Recommendation: Merge with confidence. 🚀 📝 Minor Nits (Optional)
Great work solving this long-standing issue! The React 17+ breaking change made the old workaround obsolete, and this Motion-native solution is the right approach. 🤖 Review conducted by Claude Code |
Summary
propagateprop to control gesture event propagation between nested motion componentspropagate={{ tap: false }}prevents parent tap gesture handlers (onTap,onTapStart,whileTap) from firing when a child is tappedonPointerDownCapture+stopPropagation()workaround is broken in React 17+WeakSet<Event>to track "claimed" pointerdown events, avoiding nativestopPropagation()entirely so other non-Motion listeners are unaffectedhover,drag, etc.)stopPropagationoption on the motion-dompress()function for framework-agnostic usageUsage
Test plan
propagate={{ tap: false }}fires childonTapbut prevents parentonTaponTapfire (default preserved)whileTapanimation isolated to child when propagation disabledstopPropagationoption onpress()prevents parent handlerGenerated with Claude Code