Skip to content

refactor: optimize animation hooks and improve conditional aria-label handling#78

Merged
shubug1015 merged 6 commits intomainfrom
refactor/performance-and-a11y
Jan 13, 2026
Merged

refactor: optimize animation hooks and improve conditional aria-label handling#78
shubug1015 merged 6 commits intomainfrom
refactor/performance-and-a11y

Conversation

@shubug1015
Copy link
Copy Markdown
Owner

@shubug1015 shubug1015 commented Jan 13, 2026

Description

This PR refactors focusing on performance optimizations and accessibility improvements in animation-related components and hooks.

  1. Performance optimizations

    • Optimized dependency management in useAnimatedChildren by leveraging useRef to avoid unnecessary recalculations.
    • Optimized onAnimationStart handling in TextMotion using useRef, preventing redundant re-creation of callback logic across renders.
    • These changes reduce unnecessary re-renders while preserving existing behavior.
  2. Accessibility improvements

    • Introduced a dedicated accessibility utility to handle aria-label logic.
    • Applied conditional aria-label assignment to ensure accessibility attributes are only set when meaningful.
    • This avoids redundant or misleading ARIA attributes while maintaining screen reader compatibility.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor

Checklist

  • My code follows the project style guidelines
  • I performed a self-review of my own code
  • I added tests that prove my fix is effective
  • I added necessary documentation

Summary by CodeRabbit

  • Bug Fixes

    • Improved accessibility for animated text via dynamic aria-label handling.
    • Optimized animation callbacks to avoid unnecessary re-computations.
  • New Features

    • Added utility to generate aria-label attributes based on text content.
  • Refactor

    • Removed line-based text splitting — split now supports only "character" and "word".
  • Documentation

    • Updated docs and prop descriptions to reflect supported split options.
  • Tests

    • Added aria-label tests; removed/disabled tests related to the "line" split.
  • Chores

    • Bumped package version to 0.0.10

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

@shubug1015 shubug1015 requested a review from kjyong702 January 13, 2026 08:56
@shubug1015 shubug1015 self-assigned this Jan 13, 2026
@shubug1015 shubug1015 added enhancement New feature or request refactor Refactor code labels Jan 13, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

Removes the "line" split option across types, validation, docs, tests, and split logic; adds getAriaLabel accessibility util; and stabilizes onAnimationStart/onAnimationEnd via refs to avoid re-running memoized animation child computation.

Changes

Cohort / File(s) Summary
Documentation
README.md
Removed "line" from examples and split prop descriptions; updated wording and defaults.
Types
src/types/common.ts
Split type changed from `'character'
Component
src/components/TextMotion/TextMotion.tsx
Use useRef for onAnimationStart, import/use getAriaLabel, replace static aria-label with dynamic ariaProps spread; minor description updates.
Component Tests
src/components/TextMotion/TextMotion.spec.tsx
Commented-out test that warned about split="line" with non-string children.
Hooks — animation
src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
Introduced onAnimationEndRef and sync effect; pass ref into wrapWithAnimatedSpan; removed onAnimationEnd from memo deps; use ref.current in callbacks.
Hooks — validation
src/hooks/useValidation/validation.ts, src/hooks/useValidation/validation.spec.tsx, src/hooks/useValidation/useValidation.spec.tsx
Removed 'line' from allowed splits and tightened related tests; disabled split="line" warning tests.
Accessibility utils
src/utils/accessibility/accessibility.ts, src/utils/accessibility/accessibility.spec.ts, src/utils/accessibility/index.ts
Added getAriaLabel(text) returning { 'aria-label': text } for non-empty trimmed text; added tests and barrel export.
Text splitting
src/utils/splitText/splitText.ts, src/utils/splitText/splitText.spec.ts
Removed/disabled line handling and corresponding test; docs/comments reflect only word/character.
Manifest
package.json
Bumped package version 0.0.9 → 0.0.10.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant TextMotion
    participant useAnimatedChildren
    participant wrapWithAnimatedSpan
    participant DOM

    User->>TextMotion: render(children, split, onAnimationStart, onAnimationEnd)
    TextMotion->>useAnimatedChildren: compute animated children (memoized)
    useAnimatedChildren->>wrapWithAnimatedSpan: wrap nodes (passes onAnimationEndRef)
    wrapWithAnimatedSpan->>DOM: create animated spans (attach end callback via ref)
    TextMotion->>TextMotion: sync onAnimationStart -> onAnimationStartRef
    DOM-->>TextMotion: animationstart event -> onAnimationStartRef.current()
    DOM-->>useAnimatedChildren: animationend event -> onAnimationEndRef.current()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • kjyong702
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description comprehensively covers the PR objectives with clear sections on performance optimizations, accessibility improvements, and type of change. However, it does not mention the removal of 'line' split support, which represents a breaking API change affecting the Split type. Consider clarifying whether the removal of 'line' split support is intentional and should be documented, and whether this constitutes a breaking change that should be noted separately.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main refactoring work: optimization of animation hooks (useAnimatedChildren, onAnimationStart with useRef) and conditional aria-label handling improvements, matching the core changes across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db57006 and 91d6102.

📒 Files selected for processing (1)
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json

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

@codecov
Copy link
Copy Markdown

codecov bot commented Jan 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@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: 0

Caution

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

⚠️ Outside diff range comments (2)
src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (1)

44-64: Potential stale closure: ref is dereferenced at memo time, not invocation time.

The optimization captures onAnimationEndRef.current when the useMemo runs (line 60), not when the animation actually ends. If the onAnimationEnd prop changes after memoization but before animation completion, the old callback executes.

To fix, pass the ref object to wrapWithAnimatedSpan and dereference .current at the invocation site (line 85).

Proposed fix
  const animatedChildren = useMemo(() => {
    const totalNodes = countNodes(splittedNode);

    const { nodes } = wrapWithAnimatedSpan(
      splittedNode,
      0,
      initialDelay,
      animationOrder,
      resolvedMotion,
      totalNodes,
-     onAnimationEndRef.current
+     onAnimationEndRef
    );

    return nodes;
  }, [splittedNode, initialDelay, animationOrder, resolvedMotion]);

Then update wrapWithAnimatedSpan signature and usage:

 const wrapWithAnimatedSpan = (
   splittedNode: ReactNode[],
   currentSequenceIndex: number,
   initialDelay: number,
   animationOrder: AnimationOrder,
   resolvedMotion: Motion,
   totalNodes: number,
-  onAnimationEnd?: () => void
+  onAnimationEndRef?: React.RefObject<(() => void) | undefined>
 ): WrapResult => {
   // ...
-      const handleAnimationEnd = isLast ? onAnimationEnd : undefined;
+      const handleAnimationEnd = isLast ? () => onAnimationEndRef?.current?.() : undefined;
src/utils/splitText/splitText.ts (1)

8-8: Update JSDoc to reflect removal of 'line' split type.

The documentation still lists line as a valid split type, but the implementation no longer supports it. This inconsistency will confuse API consumers.

📝 Suggested fix
- * @param {Split} split - The split type (`character`, `word`, or `line`).
+ * @param {Split} split - The split type (`character` or `word`).
🧹 Nitpick comments (8)
src/utils/accessibility/accessibility.spec.ts (1)

8-10: Consider adding a whitespace-only test case.

The current tests cover non-empty and empty strings, but a whitespace-only input (' ') would validate the trim() behavior—ensuring it returns {} rather than an aria-label with only spaces.

Suggested additional test
  it('should return empty object when text is empty', () => {
    expect(getAriaLabel('')).toEqual({});
  });
+
+  it('should return empty object when text is whitespace only', () => {
+    expect(getAriaLabel('   ')).toEqual({});
+  });
src/components/TextMotion/TextMotion.spec.tsx (1)

265-280: Remove commented-out tests instead of leaving dead code.

If the split="line" feature is permanently removed, these commented-out tests should be deleted rather than left in place. Dead code increases cognitive load for future maintainers and contradicts the principle of keeping tests in sync with the codebase.

The same applies to the commented-out test block at lines 220-240.

♻️ Suggested approach

Delete the entire commented block (lines 265-280) and the earlier commented block (lines 220-240) completely if the "line" split feature won't be restored.

If you anticipate potentially restoring this feature, consider tracking it in an issue or ADR instead of leaving commented code in the test suite.

src/hooks/useValidation/validation.spec.tsx (1)

30-38: Remove commented-out test instead of leaving dead code.

Same concern as in TextMotion.spec.tsx — if the split="line" feature is permanently removed, delete this test block entirely rather than commenting it out.

src/utils/splitText/splitText.spec.ts (1)

7-11: Remove commented-out test case from array.

Leaving a commented line inside an array literal is particularly confusing. Delete line 10 entirely to keep the test data clean.

♻️ Proposed fix
   const testCases: [Split, string, string[]][] = [
     ['character', 'Hello', ['H', 'e', 'l', 'l', 'o']],
     ['word', 'Hello World', ['Hello', ' ', 'World']],
-    // ['line', 'Hello\nWorld', ['Hello', '\n', 'World']],
   ];
src/types/common.ts (1)

12-18: Type definition is clean, but clarify if this is an actual breaking change.

The Split type definition and JSDoc accurately describe the 'character' and 'word' options. However, the claim that removing 'line' is a breaking change needs validation:

  • No usages of split="line" or similar patterns exist in the codebase
  • The package is at version 0.0.9 (pre-1.0), so API changes are typically expected
  • There is no CHANGELOG or documentation mentioning this removal

If 'line' was previously documented as a public feature, add a note to the PR or CHANGELOG. Otherwise, this is a safe internal refinement. Confirm which applies and update the PR description accordingly.

src/utils/splitText/splitText.ts (1)

16-17: Remove commented-out code instead of leaving it in place.

Commented-out code adds noise and creates confusion about whether this is intentionally disabled or pending future work. Since 'line' support is being removed across the codebase (types, validation, tests), this dead code should be deleted.

♻️ Suggested fix
   case 'word':
     return text.split(/(\s+)/).filter(Boolean);
-  // case 'line':
-  //   return text.split(/(\n)/).filter(Boolean);
   case 'character':
   default:
     return text.split('');
src/hooks/useValidation/validation.ts (1)

25-27: Remove commented-out validation code.

Same concern as in splitText.ts — this dead code should be deleted rather than commented out. The 'line' split type is being fully removed, so this validation branch is no longer needed.

♻️ Suggested fix
   if (props.children === undefined || props.children === null) {
     warnings.push('children prop is empty');
   }
 
-  // if (props.split === 'line' && typeof props.children !== 'string') {
-  //   warnings.push('split="line" is only applicable when children is a string.');
-  // }
-
   const common = validateCommonProps(props);
src/components/TextMotion/TextMotion.tsx (1)

99-107: Remove unnecessary IIFE wrapper.

The IIFE (Immediately Invoked Function Expression) adds cognitive overhead without providing any benefit here. The ariaProps variable can be computed directly before the return statement, matching the pattern used in the non-animating branch above (lines 90-96). This improves readability by keeping consistent patterns within the component.

♻️ Suggested fix
-  return (() => {
-    const ariaProps = getAriaLabel(text || DEFAULT_ARIA_LABEL);
+  const ariaProps = getAriaLabel(text || DEFAULT_ARIA_LABEL);
 
-    return (
-      <Tag ref={targetRef} className="text-motion" {...ariaProps}>
-        {animatedChildren}
-      </Tag>
-    );
-  })();
+  return (
+    <Tag ref={targetRef} className="text-motion" {...ariaProps}>
+      {animatedChildren}
+    </Tag>
+  );
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5e57ea and 8358c99.

📒 Files selected for processing (13)
  • README.md
  • src/components/TextMotion/TextMotion.spec.tsx
  • src/components/TextMotion/TextMotion.tsx
  • src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
  • src/hooks/useValidation/useValidation.spec.tsx
  • src/hooks/useValidation/validation.spec.tsx
  • src/hooks/useValidation/validation.ts
  • src/types/common.ts
  • src/utils/accessibility/accessibility.spec.ts
  • src/utils/accessibility/accessibility.ts
  • src/utils/accessibility/index.ts
  • src/utils/splitText/splitText.spec.ts
  • src/utils/splitText/splitText.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

**/*.{ts,tsx}: You are a senior frontend reviewer.
Review this code strictly based on the principles of "easy-to-change code"
from Frontend Fundamentals (Readability, Predictability, Cohesion, Low Coupling).

================================

  1. Readability
    ================================
  • Check if the code can be read top-to-bottom like a document.
  • Flag places where the reader must jump around to understand logic.
  • Suggest extracting logic when multiple contexts are mixed in one function or component.
  • Identify magic numbers or strings and suggest meaningful named constants.
  • Point out unclear or abbreviated variable/function names.
  • Prefer simple control flow over nested or overly clever logic.

================================
2. Predictability

  • Verify that function and variable names clearly imply their behavior.
  • Ensure functions with similar responsibilities return consistent data shapes.
  • Flag hidden side effects (global state mutation, implicit behavior).
  • Warn when the same name is used with different meanings across the codebase.
  • Suggest making implicit assumptions explicit.

================================
3. Cohesion

  • Check whether code that changes together is located together.
  • Flag cases where related logic is scattered across multiple files unnecessarily.
  • Suggest grouping constants, helpers, and hooks near the domain they belong to.
  • Identify components or functions that contain unrelated responsibilities.
  • Prefer feature/domain-based structure over technical grouping when applicable.

================================
4. Coupling

  • Identify code where a small change would cause wide ripple effects.
  • Warn when abstractions increase coupling instead of reducing it.
  • Prefer duplication over premature abstraction if it lowers change impact.
  • Flag excessive prop drilling and suggest alternative patterns if appropriate.
  • Ensure compo...

Files:

  • src/utils/accessibility/index.ts
  • src/hooks/useValidation/useValidation.spec.tsx
  • src/utils/accessibility/accessibility.ts
  • src/utils/splitText/splitText.spec.ts
  • src/utils/splitText/splitText.ts
  • src/utils/accessibility/accessibility.spec.ts
  • src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
  • src/hooks/useValidation/validation.ts
  • src/components/TextMotion/TextMotion.spec.tsx
  • src/types/common.ts
  • src/components/TextMotion/TextMotion.tsx
  • src/hooks/useValidation/validation.spec.tsx
🧬 Code graph analysis (3)
src/hooks/useValidation/useValidation.spec.tsx (1)
src/utils/splitNodeAndExtractText/splitNodeAndExtractText.spec.tsx (6)
  • splitNodeAndExtractText (51-60)
  • splitNodeAndExtractText (23-28)
  • splitNodeAndExtractText (37-42)
  • splitNodeAndExtractText (76-84)
  • symbolNode (62-74)
  • splitNodeAndExtractText (44-49)
src/utils/splitText/splitText.ts (1)
src/utils/splitNodeAndExtractText/splitNodeAndExtractText.spec.tsx (5)
  • splitNodeAndExtractText (23-28)
  • splitNodeAndExtractText (51-60)
  • splitNodeAndExtractText (76-84)
  • splitNodeAndExtractText (4-11)
  • it (3-85)
src/utils/accessibility/accessibility.spec.ts (1)
src/utils/accessibility/accessibility.ts (1)
  • getAriaLabel (7-9)
🪛 LanguageTool
README.md

[style] ~98-~98: To form a complete sentence, be sure to include a subject or ‘there’.
Context: ... | Content to animate. Can be a string, a number, or any React ele...

(MISSING_IT_THERE)

⏰ 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). (1)
  • GitHub Check: Run Coverage
🔇 Additional comments (9)
src/utils/accessibility/accessibility.ts (1)

1-9: LGTM!

Clean, focused utility with a clear single responsibility. The JSDoc is helpful, and the return type correctly models the optional property. The decision to trim only for the empty check while preserving original text in the label is reasonable for accessibility.

src/utils/accessibility/index.ts (1)

1-1: LGTM!

Standard barrel export pattern that enables clean imports from the directory root.

README.md (1)

96-108: LGTM!

The API reference table correctly reflects the removal of the "line" split option. Documentation is clear and consistent with the code changes.

src/hooks/useValidation/useValidation.spec.tsx (1)

23-26: LGTM!

The updated error message expectation correctly reflects the removal of 'line' from valid split values, maintaining consistency with the updated Split type definition.

src/hooks/useValidation/validation.spec.tsx (1)

40-47: LGTM!

The valid splits array correctly reflects only 'character' and 'word', consistent with the updated Split type.

src/hooks/useValidation/validation.ts (1)

48-49: LGTM!

The validation logic correctly narrows the allowed split values to 'character' and 'word', with a clear error message. This is consistent with the type definition changes.

src/components/TextMotion/TextMotion.tsx (3)

77-87: LGTM on the useRef callback pattern.

This correctly stabilizes the onAnimationStart callback reference, preventing the effect from re-running when the callback identity changes while ensuring the latest callback is always invoked. The simplified [shouldAnimate] dependency is intentional and correct since the ref is stable.


89-96: LGTM on the ariaProps pattern.

The dynamic ariaProps spread cleanly centralizes aria-label logic and only applies the attribute when meaningful.


10-10: No action needed—the getAriaLabel export is correctly structured.

The barrel export at src/utils/accessibility/index.ts properly re-exports everything from ./accessibility, and getAriaLabel is correctly exported from the implementation file. The import path in TextMotion.tsx resolves without issue.

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In @src/hooks/useAnimatedChildren/useAnimatedChildren.tsx:
- Around line 44-48: The useMemo for AnimatedSpan is capturing
onAnimationEndRef.current at memoization time causing a stale callback; modify
the handler inside the useMemo so it reads onAnimationEndRef.current at
invocation time (e.g., replace direct reads of onAnimationEndRef.current within
the useMemo return with a closure that calls onAnimationEndRef.current() when
the animation actually ends), ensuring useAnimatedChildren's onAnimationEndRef
and the useEffect that updates it remain unchanged.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8358c99 and 221e437.

📒 Files selected for processing (2)
  • src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
  • src/utils/splitText/splitText.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/splitText/splitText.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

**/*.{ts,tsx}: You are a senior frontend reviewer.
Review this code strictly based on the principles of "easy-to-change code"
from Frontend Fundamentals (Readability, Predictability, Cohesion, Low Coupling).

================================

  1. Readability
    ================================
  • Check if the code can be read top-to-bottom like a document.
  • Flag places where the reader must jump around to understand logic.
  • Suggest extracting logic when multiple contexts are mixed in one function or component.
  • Identify magic numbers or strings and suggest meaningful named constants.
  • Point out unclear or abbreviated variable/function names.
  • Prefer simple control flow over nested or overly clever logic.

================================
2. Predictability

  • Verify that function and variable names clearly imply their behavior.
  • Ensure functions with similar responsibilities return consistent data shapes.
  • Flag hidden side effects (global state mutation, implicit behavior).
  • Warn when the same name is used with different meanings across the codebase.
  • Suggest making implicit assumptions explicit.

================================
3. Cohesion

  • Check whether code that changes together is located together.
  • Flag cases where related logic is scattered across multiple files unnecessarily.
  • Suggest grouping constants, helpers, and hooks near the domain they belong to.
  • Identify components or functions that contain unrelated responsibilities.
  • Prefer feature/domain-based structure over technical grouping when applicable.

================================
4. Coupling

  • Identify code where a small change would cause wide ripple effects.
  • Warn when abstractions increase coupling instead of reducing it.
  • Prefer duplication over premature abstraction if it lowers change impact.
  • Flag excessive prop drilling and suggest alternative patterns if appropriate.
  • Ensure compo...

Files:

  • src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
🔇 Additional comments (3)
src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (3)

64-64: Dependency removal is correct (assuming the closure fix is applied).

Removing onAnimationEnd from the dependency array is the intended optimization. Once the stale callback issue above is fixed with a closure, this correctly avoids re-computing animated children when only the callback reference changes.


69-77: LGTM on function signature and ref propagation.

The updated signature clearly accepts the ref object, and propagating it through recursive calls (Line 100) maintains consistency. This is explicit and readable.


1-1: Imports are appropriate for the ref pattern.

Adding useEffect and useRef aligns with the stabilization pattern. Per the coding guidelines on cohesion, the ref management logic stays localized within this hook.

@shubug1015 shubug1015 marked this pull request as ready for review January 13, 2026 09:29
@shubug1015 shubug1015 merged commit 4909267 into main Jan 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request refactor Refactor code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant