diff --git a/README.md b/README.md
index 243e53b..f3146a3 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ From **character-level typing effects** to **complex UI block animations**, it p
- **Lightweight & Performant** – minimal footprint, no heavy deps
- **Robust & Tested** – 100% test coverage with Jest + React Testing Library
-- **Flexible API** – animate by character, word, or line, supporting both plain text and rich React nodes.
+- **Flexible API** – animate by character or word, supporting both plain text and rich React nodes.
- **Presets & Motion** – use built-in effects or define your property values
- **Developer-Friendly** – JSDoc, examples, TypeScript support
@@ -44,7 +44,7 @@ Instantly animates `"Hello World!"` with fade + slide.
### `TextMotion`
-Animate **plain text strings** or **any React children** (mixed tags, custom components, blocks) with per-character, word, or line animations.
+Animate **plain text strings** or **any React children** (mixed tags, custom components, blocks) with per-character or word animations.
```tsx
// Animating a plain string
@@ -93,19 +93,19 @@ Animate **plain text strings** or **any React children** (mixed tags, custom com
### TextMotion Props
-| Prop | Type | Default | Required | Description |
-| ------------------ | ------------------------------------ | --------------- | ----------------------- | ---------------------------------------------------------------------- |
-| `children` | `ReactNode` | `-` | Yes | Content to animate. Can be a string, a number, or any React element. |
-| `as` | `string` | `"span"` | No | HTML tag wrapper |
-| `split` | `"character" \| "word" \| "line"` | `"character"` | No | Text split granularity. `line` is only applicable for string children. |
-| `trigger` | `"on-load" \| "scroll"` | `"scroll"` | No | When animation starts |
-| `repeat` | `boolean` | `true` | No | Repeat entire animation |
-| `initialDelay` | `number` | `0` | No | Initial delay before animation starts (in `s`) |
-| `animationOrder` | `"first-to-last" \| "last-to-first"` | `first-to-last` | No | Order of the animation sequence |
-| `motion` | `Motion` | `-` | Yes (if `preset` unset) | Custom animation config |
-| `preset` | `Preset[]` | `-` | Yes (if `motion` unset) | Predefined animation presets |
-| `onAnimationStart` | `() => void` | `-` | No | Callback function that is called when the animation starts |
-| `onAnimationEnd` | `() => void` | `-` | No | Callback function that is called when the animation ends |
+| Prop | Type | Default | Required | Description |
+| ------------------ | ------------------------------------ | --------------- | ----------------------- | -------------------------------------------------------------------- |
+| `children` | `ReactNode` | `-` | Yes | Content to animate. Can be a string, a number, or any React element. |
+| `as` | `string` | `"span"` | No | HTML tag wrapper |
+| `split` | `"character" \| "word"` | `"character"` | No | Text split granularity. |
+| `trigger` | `"on-load" \| "scroll"` | `"scroll"` | No | When animation starts |
+| `repeat` | `boolean` | `true` | No | Repeat entire animation |
+| `initialDelay` | `number` | `0` | No | Initial delay before animation starts (in `s`) |
+| `animationOrder` | `"first-to-last" \| "last-to-first"` | `first-to-last` | No | Order of the animation sequence |
+| `motion` | `Motion` | `-` | Yes (if `preset` unset) | Custom animation config |
+| `preset` | `Preset[]` | `-` | Yes (if `motion` unset) | Predefined animation presets |
+| `onAnimationStart` | `() => void` | `-` | No | Callback function that is called when the animation starts |
+| `onAnimationEnd` | `() => void` | `-` | No | Callback function that is called when the animation ends |
## Presets
diff --git a/package.json b/package.json
index 0afa190..418a999 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-textmotion",
- "version": "0.0.9",
+ "version": "0.0.10",
"description": "Lightweight yet powerful library that provides variable animation effects for React applications.",
"type": "module",
"main": "dist/index.cjs.js",
diff --git a/src/components/TextMotion/TextMotion.spec.tsx b/src/components/TextMotion/TextMotion.spec.tsx
index 7812f2d..3fe63d8 100644
--- a/src/components/TextMotion/TextMotion.spec.tsx
+++ b/src/components/TextMotion/TextMotion.spec.tsx
@@ -262,20 +262,20 @@ describe('TextMotion with different split options (component-level via hook mock
expect(animatedSpans[3].textContent).toBe('!');
});
- it('should warn when using split="line" with non-string children', () => {
- const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ // it('should warn when using split="line" with non-string children', () => {
+ // const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
- render(
-
- Hello World
-
- );
+ // render(
+ //
+ // Hello World
+ //
+ // );
- expect(consoleWarnSpy).toHaveBeenCalledWith(
- 'TextMotion validation warnings:',
- expect.arrayContaining(['split="line" is only applicable when children is a string.'])
- );
+ // expect(consoleWarnSpy).toHaveBeenCalledWith(
+ // 'TextMotion validation warnings:',
+ // expect.arrayContaining(['split="line" is only applicable when children is a string.'])
+ // );
- consoleWarnSpy.mockRestore();
- });
+ // consoleWarnSpy.mockRestore();
+ // });
});
diff --git a/src/components/TextMotion/TextMotion.tsx b/src/components/TextMotion/TextMotion.tsx
index 18feae4..eb6d2ea 100644
--- a/src/components/TextMotion/TextMotion.tsx
+++ b/src/components/TextMotion/TextMotion.tsx
@@ -1,21 +1,22 @@
import '../../styles/animations.scss';
import '../../styles/motion.scss';
-import { type FC, memo, useEffect } from 'react';
+import { type FC, memo, useEffect, useRef } from 'react';
import { DEFAULT_ARIA_LABEL } from '../../constants';
import { useTextMotionAnimation } from '../../hooks/useTextMotionAnimation';
import { useValidation } from '../../hooks/useValidation';
import type { TextMotionProps } from '../../types';
+import { getAriaLabel } from '../../utils/accessibility';
/**
* @description
* `TextMotion` is a component that animates its children by applying motion presets or custom motion configurations.
- * It can animate text nodes by splitting them into characters, words or lines, and can also animate other React elements.
+ * It can animate text nodes by splitting them into characters or words, and can also animate other React elements.
*
* @param {ReactNode} children - The content to animate. Can be a string, a number, or any React element.
* @param {ElementType} [as='span'] - The HTML tag to render. Defaults to `span`.
- * @param {Split} [split='character'] - Defines how the text is split for animation (`character`, `word`, or `line`). `line` is only applicable for string children. Defaults to `'character'`.
+ * @param {Split} [split='character'] - Defines how the text is split for animation (`character` or `word`). Defaults to `'character'`.
* @param {Trigger} [trigger='scroll'] - Defines when the animation should start. 'on-load' starts the animation immediately. 'scroll' starts the animation only when the component enters the viewport. Defaults to `'scroll'`.
* @param {boolean} [repeat=true] - Determines if the animation should repeat every time it enters the viewport. Only applicable when `trigger` is `'scroll'`. Defaults to `true`.
* @param {number} [initialDelay=0] - The initial delay before the animation starts, in seconds. Defaults to `0`.
@@ -69,28 +70,39 @@ import type { TextMotionProps } from '../../types';
*/
export const TextMotion: FC = memo(props => {
const { as: Tag = 'span', children, onAnimationStart } = props;
-
useValidation({ componentName: 'TextMotion', props });
const { shouldAnimate, targetRef, animatedChildren, text } = useTextMotionAnimation(props);
+ const onAnimationStartRef = useRef(onAnimationStart);
+
+ useEffect(() => {
+ onAnimationStartRef.current = onAnimationStart;
+ }, [onAnimationStart]);
+
useEffect(() => {
if (shouldAnimate) {
- onAnimationStart?.();
+ onAnimationStartRef.current?.();
}
- }, [shouldAnimate, onAnimationStart]);
+ }, [shouldAnimate]);
if (!shouldAnimate) {
+ const ariaProps = getAriaLabel(text || DEFAULT_ARIA_LABEL);
+
return (
-
+
{children}
);
}
- return (
-
- {animatedChildren}
-
- );
+ return (() => {
+ const ariaProps = getAriaLabel(text || DEFAULT_ARIA_LABEL);
+
+ return (
+
+ {animatedChildren}
+
+ );
+ })();
});
diff --git a/src/hooks/useAnimatedChildren/useAnimatedChildren.tsx b/src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
index ddcc825..dda1a35 100644
--- a/src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
+++ b/src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
@@ -1,4 +1,4 @@
-import { Children, cloneElement, type ReactNode, useMemo } from 'react';
+import { Children, cloneElement, type MutableRefObject, type ReactNode, useEffect, useMemo, useRef } from 'react';
import type { AnimationOrder, Motion } from '../../types';
import { countNodes } from '../../utils/countNodes';
@@ -41,6 +41,12 @@ export const useAnimatedChildren = ({
resolvedMotion,
onAnimationEnd,
}: UseAnimatedChildrenProps): ReactNode[] => {
+ const onAnimationEndRef = useRef(onAnimationEnd);
+
+ useEffect(() => {
+ onAnimationEndRef.current = onAnimationEnd;
+ }, [onAnimationEnd]);
+
const animatedChildren = useMemo(() => {
const totalNodes = countNodes(splittedNode);
@@ -51,11 +57,11 @@ export const useAnimatedChildren = ({
animationOrder,
resolvedMotion,
totalNodes,
- onAnimationEnd
+ onAnimationEndRef
);
return nodes;
- }, [splittedNode, initialDelay, animationOrder, resolvedMotion, onAnimationEnd]);
+ }, [splittedNode, initialDelay, animationOrder, resolvedMotion]);
return animatedChildren;
};
@@ -67,7 +73,7 @@ const wrapWithAnimatedSpan = (
animationOrder: AnimationOrder,
resolvedMotion: Motion,
totalNodes: number,
- onAnimationEnd?: () => void
+ onAnimationEndRef?: MutableRefObject<(() => void) | undefined>
): WrapResult => {
let sequenceIndex = currentSequenceIndex;
@@ -76,7 +82,7 @@ const wrapWithAnimatedSpan = (
const currentIndex = sequenceIndex++;
const calculatedSequenceIndex = calculateSequenceIndex(currentIndex, totalNodes, animationOrder);
const isLast = isLastNode(calculatedSequenceIndex, totalNodes);
- const handleAnimationEnd = isLast ? onAnimationEnd : undefined;
+ const handleAnimationEnd = isLast ? () => onAnimationEndRef?.current?.() : undefined;
const { style } = generateAnimation(resolvedMotion, calculatedSequenceIndex, initialDelay);
return ;
@@ -91,7 +97,7 @@ const wrapWithAnimatedSpan = (
animationOrder,
resolvedMotion,
totalNodes,
- onAnimationEnd
+ onAnimationEndRef
);
sequenceIndex = nextSequenceIndex;
diff --git a/src/hooks/useValidation/useValidation.spec.tsx b/src/hooks/useValidation/useValidation.spec.tsx
index c5bed43..f87a556 100644
--- a/src/hooks/useValidation/useValidation.spec.tsx
+++ b/src/hooks/useValidation/useValidation.spec.tsx
@@ -22,7 +22,7 @@ describe('useValidation hook', () => {
expect(consoleErrorSpy).toHaveBeenCalledWith(
'TextMotion validation errors:',
- expect.arrayContaining(['split prop must be one of: character, word, line'])
+ expect.arrayContaining(['split prop must be one of: character, word'])
);
consoleErrorSpy.mockRestore();
diff --git a/src/hooks/useValidation/validation.spec.tsx b/src/hooks/useValidation/validation.spec.tsx
index f0f85ab..ccf8ed0 100644
--- a/src/hooks/useValidation/validation.spec.tsx
+++ b/src/hooks/useValidation/validation.spec.tsx
@@ -27,18 +27,18 @@ describe('validation utility', () => {
expect(warnings).toContain('children prop is empty');
});
- it('should return a warning for "line" split with non-string children', () => {
- const props: TextMotionProps = {
- children: [],
- split: 'line',
- };
- const { warnings } = validateTextMotionProps(props);
+ // it('should return a warning for "line" split with non-string children', () => {
+ // const props: TextMotionProps = {
+ // children: [],
+ // split: 'line',
+ // };
+ // const { warnings } = validateTextMotionProps(props);
- expect(warnings).toContain('split="line" is only applicable when children is a string.');
- });
+ // expect(warnings).toContain('split="line" is only applicable when children is a string.');
+ // });
it('should accept valid split values', () => {
- (['character', 'word', 'line'] as const).forEach(split => {
+ (['character', 'word'] as const).forEach(split => {
const props: TextMotionProps = { children: 'hello', split };
const { errors } = validateTextMotionProps(props);
diff --git a/src/hooks/useValidation/validation.ts b/src/hooks/useValidation/validation.ts
index 24719ae..284bb2c 100644
--- a/src/hooks/useValidation/validation.ts
+++ b/src/hooks/useValidation/validation.ts
@@ -22,9 +22,9 @@ export const validateTextMotionProps = (props: TextMotionProps): ValidationResul
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.');
- }
+ // if (props.split === 'line' && typeof props.children !== 'string') {
+ // warnings.push('split="line" is only applicable when children is a string.');
+ // }
const common = validateCommonProps(props);
@@ -45,8 +45,8 @@ const validateCommonProps = (props: Partial): ValidationResult
const errors: string[] = [];
const warnings: string[] = [];
- if (props.split !== undefined && !['character', 'word', 'line'].includes(props.split)) {
- errors.push('split prop must be one of: character, word, line');
+ if (props.split !== undefined && !['character', 'word'].includes(props.split)) {
+ errors.push('split prop must be one of: character, word');
}
if (props.trigger !== undefined && !['on-load', 'scroll'].includes(props.trigger)) {
diff --git a/src/types/common.ts b/src/types/common.ts
index 95df3b4..84197e6 100644
--- a/src/types/common.ts
+++ b/src/types/common.ts
@@ -14,9 +14,8 @@ import type {
* Defines how text is split for animation.
* - `character`: Splits the text into individual characters.
* - `word`: Splits the text into words.
- * - `line`: Splits the text into lines.
*/
-export type Split = 'character' | 'word' | 'line';
+export type Split = 'character' | 'word';
/**
* @description
diff --git a/src/utils/accessibility/accessibility.spec.ts b/src/utils/accessibility/accessibility.spec.ts
new file mode 100644
index 0000000..e3e8c91
--- /dev/null
+++ b/src/utils/accessibility/accessibility.spec.ts
@@ -0,0 +1,11 @@
+import { getAriaLabel } from './accessibility';
+
+describe('getAriaLabel', () => {
+ it('should return aria-label when text is provided', () => {
+ expect(getAriaLabel('Hello')).toEqual({ 'aria-label': 'Hello' });
+ });
+
+ it('should return empty object when text is empty', () => {
+ expect(getAriaLabel('')).toEqual({});
+ });
+});
diff --git a/src/utils/accessibility/accessibility.ts b/src/utils/accessibility/accessibility.ts
new file mode 100644
index 0000000..b7ff8c2
--- /dev/null
+++ b/src/utils/accessibility/accessibility.ts
@@ -0,0 +1,9 @@
+/**
+ * Generates aria-label attribute for accessibility
+ *
+ * @param text - The text to be used as aria-label
+ * @returns Object with aria-label attribute
+ */
+export const getAriaLabel = (text: string): { 'aria-label'?: string } => {
+ return text.trim() ? { 'aria-label': text } : {};
+};
diff --git a/src/utils/accessibility/index.ts b/src/utils/accessibility/index.ts
new file mode 100644
index 0000000..7ac25be
--- /dev/null
+++ b/src/utils/accessibility/index.ts
@@ -0,0 +1 @@
+export * from './accessibility';
diff --git a/src/utils/splitText/splitText.spec.ts b/src/utils/splitText/splitText.spec.ts
index 66f1d22..c7f3803 100644
--- a/src/utils/splitText/splitText.spec.ts
+++ b/src/utils/splitText/splitText.spec.ts
@@ -7,7 +7,7 @@ describe('splitText utility', () => {
const testCases: [Split, string, string[]][] = [
['character', 'Hello', ['H', 'e', 'l', 'l', 'o']],
['word', 'Hello World', ['Hello', ' ', 'World']],
- ['line', 'Hello\nWorld', ['Hello', '\n', 'World']],
+ // ['line', 'Hello\nWorld', ['Hello', '\n', 'World']],
];
it.each(testCases)('should split the text by %s', (split, input, expected) => {
diff --git a/src/utils/splitText/splitText.ts b/src/utils/splitText/splitText.ts
index 46d7b84..54e9c8b 100644
--- a/src/utils/splitText/splitText.ts
+++ b/src/utils/splitText/splitText.ts
@@ -5,7 +5,7 @@ import type { Split } from '../../types';
* `splitText` is a utility function that splits a string into an array of substrings based on the specified split type.
*
* @param {string} text - The text to be split.
- * @param {Split} split - The split type (`character`, `word`, or `line`).
+ * @param {Split} split - The split type (`character` or `word`).
*
* @returns {string[]} An array of substrings.
*/
@@ -13,8 +13,8 @@ export const splitText = (text: string, split: Split): string[] => {
switch (split) {
case 'word':
return text.split(/(\s+)/).filter(Boolean);
- case 'line':
- return text.split(/(\n)/).filter(Boolean);
+ // case 'line':
+ // return text.split(/(\n)/).filter(Boolean);
case 'character':
default:
return text.split('');