From 0873d2ed1bd193238282150be354e59c44e9514a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 03:39:03 +0000
Subject: [PATCH 1/6] Remove Children.map usage and implement alternative
approach
Co-authored-by: morganney <848178+morganney@users.noreply.github.com>
---
packages/tts-react/__tests__/hook.tsx | 56 +++++++++
packages/tts-react/__tests__/utils.ts | 44 ++++++-
packages/tts-react/src/hook.tsx | 174 +++++++++++++-------------
packages/tts-react/src/utils.ts | 32 ++++-
4 files changed, 215 insertions(+), 91 deletions(-)
diff --git a/packages/tts-react/__tests__/hook.tsx b/packages/tts-react/__tests__/hook.tsx
index f202383..32d51d1 100644
--- a/packages/tts-react/__tests__/hook.tsx
+++ b/packages/tts-react/__tests__/hook.tsx
@@ -321,4 +321,60 @@ describe('useTts', () => {
global.dispatchEvent(new Event('beforeunload'))
expect(global.speechSynthesis.cancel).toHaveBeenCalled()
})
+
+ it('accepts text prop as an alternative to children', async () => {
+ const testText = 'This is direct text for TTS'
+ const { result } = renderHook(
+ ({ text }) => useTts({ text }),
+ {
+ initialProps: {
+ text: testText
+ }
+ }
+ )
+
+ expect(result.current.spokenText).toBe(testText)
+ expect(result.current.ttsChildren).toBe(testText)
+ })
+
+ it('prefers text prop over children when both are provided', async () => {
+ const testText = 'Direct text prop'
+ const childrenText = 'Children text'
+ const { result } = renderHook(
+ ({ text, children }) => useTts({ text, children }),
+ {
+ initialProps: {
+ text: testText,
+ children:
{childrenText}
+ }
+ }
+ )
+
+ expect(result.current.spokenText).toBe(testText)
+ })
+
+ it('supports render prop for custom highlighting', async () => {
+ const testText = 'Test text for highlighting'
+ const mockRender = jest.fn((params) => params.children)
+
+ const { result } = renderHook(
+ ({ text, render, markTextAsSpoken }) => useTts({ text, render, markTextAsSpoken }),
+ {
+ initialProps: {
+ text: testText,
+ render: mockRender,
+ markTextAsSpoken: true
+ }
+ }
+ )
+
+ expect(result.current.spokenText).toBe(testText)
+ expect(mockRender).toHaveBeenCalledWith({
+ children: testText,
+ boundary: expect.any(Object),
+ markTextAsSpoken: true,
+ markColor: undefined,
+ markBackgroundColor: undefined
+ })
+ })
})
diff --git a/packages/tts-react/__tests__/utils.ts b/packages/tts-react/__tests__/utils.ts
index c06830a..161ff56 100644
--- a/packages/tts-react/__tests__/utils.ts
+++ b/packages/tts-react/__tests__/utils.ts
@@ -1,9 +1,49 @@
-import { describe } from '@jest/globals'
+import { describe, test } from '@jest/globals'
+import { createElement } from 'react'
-import { isPunctuation } from '../src/utils.js'
+import { isPunctuation, extractTextFromChildren } from '../src/utils.js'
describe('utils', () => {
it('has isPunctuation', () => {
expect(isPunctuation('?')).toBe(true)
})
+
+ describe('extractTextFromChildren', () => {
+ test('extracts text from string children', () => {
+ expect(extractTextFromChildren('hello world')).toBe('hello world')
+ })
+
+ test('extracts text from number children', () => {
+ expect(extractTextFromChildren(42)).toBe('42')
+ })
+
+ test('extracts text from array of children', () => {
+ expect(extractTextFromChildren(['hello', ' ', 'world'])).toBe('hello world')
+ })
+
+ test('extracts text from React elements', () => {
+ const element = createElement('p', {}, 'Hello from element')
+ expect(extractTextFromChildren(element)).toBe('Hello from element')
+ })
+
+ test('extracts text from nested React elements', () => {
+ const nested = createElement('span', {}, 'nested text')
+ const element = createElement('p', {}, 'Hello ', nested, ' world')
+ expect(extractTextFromChildren(element)).toBe('Hello nested text world')
+ })
+
+ test('handles null and undefined children', () => {
+ expect(extractTextFromChildren(null)).toBe('')
+ expect(extractTextFromChildren(undefined)).toBe('')
+ })
+
+ test('handles boolean children', () => {
+ expect(extractTextFromChildren(true)).toBe('')
+ expect(extractTextFromChildren(false)).toBe('')
+ })
+
+ test('handles empty array', () => {
+ expect(extractTextFromChildren([])).toBe('')
+ })
+ })
})
diff --git a/packages/tts-react/src/hook.tsx b/packages/tts-react/src/hook.tsx
index edf756e..c9b89c9 100644
--- a/packages/tts-react/src/hook.tsx
+++ b/packages/tts-react/src/hook.tsx
@@ -4,17 +4,14 @@ import {
useReducer,
useCallback,
useEffect,
- Children,
- cloneElement,
createElement,
- isValidElement,
Fragment
} from 'react'
import type { ReactNode } from 'react'
import { Controller, ControllerStub, Events } from './controller.js'
import type { ControllerOptions, TTSBoundaryUpdate, TTSEvent } from './controller.js'
-import { isStringOrNumber, stripPunctuation, noop } from './utils.js'
+import { stripPunctuation, noop, extractTextFromChildren } from './utils.js'
import { Highlighter } from './highlighter.js'
/**
@@ -78,9 +75,24 @@ interface MarkStyles {
/** Background color of the currently marked word. */
markBackgroundColor?: string
}
+/**
+ * Render prop function type for custom text highlighting
+ */
+type TTSRenderProp = (params: {
+ children: ReactNode
+ boundary: TTSBoundaryUpdate
+ markTextAsSpoken: boolean
+ markColor?: string
+ markBackgroundColor?: string
+}) => ReactNode
+
interface TTSHookProps extends MarkStyles {
/** The spoken text is extracted from here. */
- children: ReactNode
+ children?: ReactNode
+ /** Direct text to be spoken - alternative to children. */
+ text?: string
+ /** Render prop for custom highlighting - receives highlighting parameters */
+ render?: TTSRenderProp
/** The `SpeechSynthesisUtterance.lang` to use. */
lang?: ControllerOptions['lang']
/** The `SpeechSynthesisUtterance.voice` to use. */
@@ -175,82 +187,61 @@ interface TTSHookResponse {
/** The original children with a possible included if using `markTextAsSpoken`. */
ttsChildren: ReactNode
}
-interface TextBuffer {
- text: string
-}
-interface ParseChildrenProps extends MarkStyles {
- children: ReactNode
- buffer: TextBuffer
- boundary: TTSBoundaryUpdate
- markTextAsSpoken: boolean
-}
+/**
+ * Creates highlighted text using render prop pattern instead of Children.map
+ */
+const createHighlightedContent = (
+ children: ReactNode,
+ text: string,
+ boundary: TTSBoundaryUpdate,
+ markTextAsSpoken: boolean,
+ markColor?: string,
+ markBackgroundColor?: string,
+ renderProp?: TTSRenderProp
+): ReactNode => {
+ // If a render prop is provided, use it
+ if (renderProp) {
+ return renderProp({
+ children,
+ boundary,
+ markTextAsSpoken,
+ markColor,
+ markBackgroundColor
+ })
+ }
-const isObjectReactNode = (value: unknown): value is Record =>
- typeof value === 'object' && value !== null
-const parseChildrenRecursively = ({
- children,
- buffer,
- boundary,
- markColor,
- markBackgroundColor,
- markTextAsSpoken
-}: ParseChildrenProps): ReactNode => {
- return Children.map(children, (child) => {
- let currentChild = child
-
- if (isValidElement(child)) {
- const childProps = isObjectReactNode(child.props) ? child.props : {}
-
- currentChild = cloneElement(child, {
- ...childProps,
- // @ts-expect-error - `children` is not a valid prop for ReactElement
- children: parseChildrenRecursively({
- buffer,
- boundary,
- markColor,
- markBackgroundColor,
- markTextAsSpoken,
- children: childProps.children
- })
- })
- }
+ // If not marking text as spoken or no boundary word, return original children
+ if (!markTextAsSpoken || !boundary.word || !children) {
+ return children
+ }
- if (isStringOrNumber(child)) {
- const text = (child as string | number).toString()
- const { word, startChar, endChar } = boundary
- const bufferTextLength = buffer.text.length
-
- buffer.text += `${text} `
-
- if (markTextAsSpoken && word) {
- const start = startChar - bufferTextLength
- const end = endChar - bufferTextLength
- const prev = text.substring(0, start)
- const found = text.substring(start, end)
- const after = text.substring(end, text.length)
-
- if (found) {
- const Highlight = createElement(Highlighter, {
- text: found,
- mark: stripPunctuation(found),
- color: markColor,
- backgroundColor: markBackgroundColor
- })
- const Highlighted = createElement(
- Fragment,
- { key: `tts-${start}-${end}` },
- prev,
- Highlight,
- after
- )
-
- return Highlighted
- }
- }
+ // For simple text content, apply highlighting directly
+ if (typeof children === 'string') {
+ const { word, startChar, endChar } = boundary
+ const prev = text.substring(0, startChar)
+ const found = text.substring(startChar, endChar)
+ const after = text.substring(endChar)
+
+ if (found && stripPunctuation(found) === stripPunctuation(word)) {
+ const Highlight = createElement(Highlighter, {
+ text: found,
+ mark: stripPunctuation(found),
+ color: markColor,
+ backgroundColor: markBackgroundColor
+ })
+
+ return createElement(
+ Fragment,
+ { key: `tts-${startChar}-${endChar}` },
+ prev,
+ Highlight,
+ after
+ )
}
+ }
- return currentChild
- })
+ // For complex content, return as-is (highlighting would need custom render prop)
+ return children
}
const defaultBoundary = { word: '', startChar: 0, endChar: 0 }
const reducer = (state: TTSHookState, action: Action): TTSHookState => {
@@ -305,6 +296,8 @@ const useTts = ({
volume,
voice,
children,
+ text,
+ render,
markColor,
markBackgroundColor,
onStart,
@@ -334,20 +327,24 @@ const useTts = ({
isReady: isSynthSupported && typeof fetchAudioData === 'undefined'
})
const [ttsChildren, spokenText] = useMemo(() => {
- const buffer: TextBuffer = { text: '' }
- const parsed = parseChildrenRecursively({
- children,
- buffer,
+ // Use text prop if provided, otherwise extract from children
+ const extractedText = text || (children ? extractTextFromChildren(children) : '')
+
+ // Create highlighted content using the new approach
+ const highlightedContent = createHighlightedContent(
+ children || extractedText,
+ extractedText,
+ state.boundary,
+ markTextAsSpoken,
markColor,
markBackgroundColor,
- markTextAsSpoken,
- boundary: state.boundary
- })
+ render
+ )
- spokenTextRef.current = buffer.text.trim()
+ spokenTextRef.current = extractedText.trim()
- return [parsed, spokenTextRef.current]
- }, [children, state.boundary, markColor, markBackgroundColor, markTextAsSpoken])
+ return [highlightedContent, spokenTextRef.current]
+ }, [children, text, state.boundary, markColor, markBackgroundColor, markTextAsSpoken, render])
const controller = useMemo(
() =>
isSynthSupported
@@ -674,5 +671,6 @@ export type {
TTSEventHandler,
TTSErrorHandler,
TTSBoundaryHandler,
- TTSAudioChangeHandler
+ TTSAudioChangeHandler,
+ TTSRenderProp
}
diff --git a/packages/tts-react/src/utils.ts b/packages/tts-react/src/utils.ts
index 57ee275..1847e33 100644
--- a/packages/tts-react/src/utils.ts
+++ b/packages/tts-react/src/utils.ts
@@ -1,4 +1,5 @@
import type { ReactNode } from 'react'
+import { isValidElement } from 'react'
const punctuationRgx = /[^\P{P}'/-]+/gu
const isStringOrNumber = (value: ReactNode): boolean => {
@@ -14,4 +15,33 @@ const isPunctuation = (text: string): boolean => {
}
const noop = (): void => {}
-export { isStringOrNumber, stripPunctuation, isPunctuation, noop, punctuationRgx }
+/**
+ * Extracts text content from React children without using Children.map
+ * This is an alternative to the legacy Children.map API
+ */
+const extractTextFromChildren = (children: ReactNode): string => {
+ if (!children) {
+ return ''
+ }
+
+ // Handle string and number primitives
+ if (typeof children === 'string' || typeof children === 'number') {
+ return children.toString()
+ }
+
+ // Handle arrays of children
+ if (Array.isArray(children)) {
+ return children.map((child: ReactNode) => extractTextFromChildren(child)).join(' ')
+ }
+
+ // Handle React elements
+ if (isValidElement(children) && children.props && 'children' in children.props) {
+ // For React elements, recursively extract text from their children
+ return extractTextFromChildren(children.props.children as ReactNode)
+ }
+
+ // Handle other types (null, undefined, boolean, etc.)
+ return ''
+}
+
+export { isStringOrNumber, stripPunctuation, isPunctuation, noop, punctuationRgx, extractTextFromChildren }
From fde9f3f09667b3911325f02c199b70284d61f3f6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 03:40:38 +0000
Subject: [PATCH 2/6] Fix TypeScript compilation error in text extraction
Co-authored-by: morganney <848178+morganney@users.noreply.github.com>
---
packages/tts-react/src/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/tts-react/src/utils.ts b/packages/tts-react/src/utils.ts
index 1847e33..ecb03bd 100644
--- a/packages/tts-react/src/utils.ts
+++ b/packages/tts-react/src/utils.ts
@@ -35,7 +35,7 @@ const extractTextFromChildren = (children: ReactNode): string => {
}
// Handle React elements
- if (isValidElement(children) && children.props && 'children' in children.props) {
+ if (isValidElement(children) && children.props && typeof children.props === 'object' && children.props !== null && 'children' in children.props) {
// For React elements, recursively extract text from their children
return extractTextFromChildren(children.props.children as ReactNode)
}
From 574f50821d91a333d06b7f5e70e8de119d68bd62 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 04:20:35 +0000
Subject: [PATCH 3/6] Fix TypeScript lint error in test file for render prop
mock
Co-authored-by: morganney <848178+morganney@users.noreply.github.com>
---
packages/tts-react/__tests__/hook.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/tts-react/__tests__/hook.tsx b/packages/tts-react/__tests__/hook.tsx
index 32d51d1..7c3faec 100644
--- a/packages/tts-react/__tests__/hook.tsx
+++ b/packages/tts-react/__tests__/hook.tsx
@@ -4,6 +4,7 @@ import { renderHook, act, waitFor } from '@testing-library/react'
import { SpeechSynthesisMock } from './speechSynthesis.mock.js'
import { SpeechSynthesisEventMock } from './speechSynthesisEvent.mock.js'
import { useTts } from '../src/hook.js'
+import type { TTSRenderProp } from '../src/hook.js'
import { stripPunctuation } from '../src/utils.js'
import './setup.js'
@@ -355,7 +356,7 @@ describe('useTts', () => {
it('supports render prop for custom highlighting', async () => {
const testText = 'Test text for highlighting'
- const mockRender = jest.fn((params) => params.children)
+ const mockRender = jest.fn, Parameters>((params: Parameters[0]) => params.children)
const { result } = renderHook(
({ text, render, markTextAsSpoken }) => useTts({ text, render, markTextAsSpoken }),
From f86129cb45e73c89b865db9ef3712595764e0732 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 04:30:29 +0000
Subject: [PATCH 4/6] Fix TypeScript lint error in render prop test by
replacing expect.any with type checking
Co-authored-by: morganney <848178+morganney@users.noreply.github.com>
---
packages/tts-react/__tests__/hook.tsx | 19 ++++++++++++-------
1 file changed, 12 insertions(+), 7 deletions(-)
diff --git a/packages/tts-react/__tests__/hook.tsx b/packages/tts-react/__tests__/hook.tsx
index 7c3faec..c36a63d 100644
--- a/packages/tts-react/__tests__/hook.tsx
+++ b/packages/tts-react/__tests__/hook.tsx
@@ -370,12 +370,17 @@ describe('useTts', () => {
)
expect(result.current.spokenText).toBe(testText)
- expect(mockRender).toHaveBeenCalledWith({
- children: testText,
- boundary: expect.any(Object),
- markTextAsSpoken: true,
- markColor: undefined,
- markBackgroundColor: undefined
- })
+ expect(mockRender).toHaveBeenCalledTimes(1)
+ const callArgs = mockRender.mock.calls[0][0]
+ expect(callArgs.children).toBe(testText)
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ expect(typeof callArgs.boundary.word).toBe('string')
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ expect(typeof callArgs.boundary.startChar).toBe('number')
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ expect(typeof callArgs.boundary.endChar).toBe('number')
+ expect(callArgs.markTextAsSpoken).toBe(true)
+ expect(callArgs.markColor).toBeUndefined()
+ expect(callArgs.markBackgroundColor).toBeUndefined()
})
})
From 70d896ee0df900d5906422fe79fe859b7f32b54e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 Aug 2025 02:54:27 +0000
Subject: [PATCH 5/6] Add default render prop implementation to restore
highlighting for complex content
Co-authored-by: morganney <848178+morganney@users.noreply.github.com>
---
packages/tts-react/src/component.tsx | 107 ++++++++++++++++++++++++++-
1 file changed, 104 insertions(+), 3 deletions(-)
diff --git a/packages/tts-react/src/component.tsx b/packages/tts-react/src/component.tsx
index 6c779a0..a74cde3 100644
--- a/packages/tts-react/src/component.tsx
+++ b/packages/tts-react/src/component.tsx
@@ -1,8 +1,11 @@
-import { useMemo, useCallback } from 'react'
-import type { CSSProperties } from 'react'
+import { useMemo, useCallback, createElement, cloneElement, Fragment, isValidElement } from 'react'
+import type { CSSProperties, ReactNode } from 'react'
import { useTts } from './hook.js'
-import type { TTSHookProps } from './hook.js'
+import type { TTSHookProps, TTSRenderProp } from './hook.js'
+import type { TTSBoundaryUpdate } from './controller.js'
+import { isStringOrNumber, stripPunctuation, extractTextFromChildren } from './utils.js'
+import { Highlighter } from './highlighter.js'
import { iconSizes, Sizes } from './icons.js'
import type { SvgProps } from './icons.js'
import { Control, padding as ctrlPadding } from './control.js'
@@ -103,6 +106,90 @@ const content = (): CSSProperties => {
gridArea: 'cnt'
}
}
+
+/**
+ * Default render prop implementation that handles complex React children
+ * by recursively applying highlighting similar to the old Children.map approach
+ */
+const createDefaultRenderer = (
+ text: string,
+ markColor?: string,
+ markBackgroundColor?: string
+): TTSRenderProp => {
+ let textBuffer = ''
+
+ const parseChildrenRecursively = (
+ children: ReactNode,
+ boundary: TTSBoundaryUpdate,
+ markTextAsSpoken: boolean
+ ): ReactNode => {
+ if (!children) return children
+
+ // Handle arrays of children
+ if (Array.isArray(children)) {
+ return children.map((child: ReactNode) =>
+ parseChildrenRecursively(child, boundary, markTextAsSpoken)
+ )
+ }
+
+ // Handle React elements
+ if (isValidElement(children)) {
+ const childProps = children.props && typeof children.props === 'object' ? children.props : {}
+
+ return cloneElement(children, {
+ ...childProps,
+ children: parseChildrenRecursively(
+ 'children' in childProps ? (childProps.children as ReactNode) : undefined,
+ boundary,
+ markTextAsSpoken
+ )
+ } as Record)
+ }
+
+ // Handle string and number primitives
+ if (isStringOrNumber(children)) {
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ const childText = String(children)
+ const { word, startChar, endChar } = boundary
+ const bufferTextLength = textBuffer.length
+
+ textBuffer += `${childText} `
+
+ if (markTextAsSpoken && word) {
+ const start = startChar - bufferTextLength
+ const end = endChar - bufferTextLength
+ const prev = childText.substring(0, start)
+ const found = childText.substring(start, end)
+ const after = childText.substring(end, childText.length)
+
+ if (found && stripPunctuation(found) === stripPunctuation(word)) {
+ const Highlight = createElement(Highlighter, {
+ text: found,
+ mark: stripPunctuation(found),
+ color: markColor,
+ backgroundColor: markBackgroundColor
+ })
+
+ return createElement(
+ Fragment,
+ { key: `tts-${start}-${end}` },
+ prev,
+ Highlight,
+ after
+ )
+ }
+ }
+ }
+
+ return children
+ }
+
+ return ({ children, boundary, markTextAsSpoken }) => {
+ // Reset the buffer for each render
+ textBuffer = ''
+ return parseChildrenRecursively(children, boundary, markTextAsSpoken)
+ }
+}
/**
* `useTts` is a React hook for converting text to speech using
* the `SpeechSynthesis` and `SpeechSynthesisUtterance` Browser API's.
@@ -128,6 +215,8 @@ const TextToSpeech = ({
voice,
volume,
children,
+ text,
+ render,
position,
onStart,
onPause,
@@ -148,12 +237,24 @@ const TextToSpeech = ({
markTextAsSpoken = false,
useStopOverPause = false
}: TTSProps) => {
+ // Create a default renderer for complex content when markTextAsSpoken is true
+ // but no custom render prop is provided
+ const defaultRenderer = useMemo(() => {
+ if (markTextAsSpoken && !render) {
+ const extractedText = text || (children ? extractTextFromChildren(children) : '')
+ return createDefaultRenderer(extractedText, markColor, markBackgroundColor)
+ }
+ return undefined
+ }, [markTextAsSpoken, render, text, children, markColor, markBackgroundColor])
+
const { state, replay, toggleMute, playOrPause, playOrStop, ttsChildren } = useTts({
lang,
rate,
voice,
volume,
children,
+ text,
+ render: render || defaultRenderer,
onStart,
onPause,
onBoundary,
From 727413d1fa523aa5405fe30e2bd26e39aee7179c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 Aug 2025 03:42:05 +0000
Subject: [PATCH 6/6] Add comprehensive render.story.tsx demonstrating render
prop usage
Co-authored-by: morganney <848178+morganney@users.noreply.github.com>
---
packages/story/src/render.story.tsx | 407 ++++++++++++++++++++++++++++
1 file changed, 407 insertions(+)
create mode 100644 packages/story/src/render.story.tsx
diff --git a/packages/story/src/render.story.tsx b/packages/story/src/render.story.tsx
new file mode 100644
index 0000000..70da082
--- /dev/null
+++ b/packages/story/src/render.story.tsx
@@ -0,0 +1,407 @@
+import type { Meta, StoryFn } from '@storybook/react'
+import type { ReactNode } from 'react'
+
+import { TextToSpeech, Positions, Sizes } from 'tts-react'
+
+/**
+ * Custom highlight component with gradient background
+ */
+const GradientHighlight = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+)
+
+/**
+ * Custom highlight component with pulsing animation
+ */
+const PulsingHighlight = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+
+)
+
+/**
+ * Custom highlight component with border styling
+ */
+const BorderHighlight = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+)
+
+/**
+ * Basic render prop example with custom highlighting
+ */
+const BasicRenderProp: StoryFn = (args) => {
+ return (
+
+
Basic Custom Render Prop
+
This example demonstrates a custom render prop that applies gradient highlighting to spoken words.
+
{
+ if (!markTextAsSpoken || !boundary.word) {
+ return children
+ }
+
+ // Extract the spoken word and apply custom highlighting
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ const text = typeof children === 'string' ? children : String(children)
+ const { startChar, endChar } = boundary
+ const before = text.substring(0, startChar)
+ const spoken = text.substring(startChar, endChar)
+ const after = text.substring(endChar)
+
+ return (
+ <>
+ {before}
+ {spoken}
+ {after}
+ >
+ )
+ }}
+ />
+
+ )
+}
+
+/**
+ * Animated render prop example
+ */
+const AnimatedRenderProp: StoryFn = (args) => {
+ return (
+
+
Animated Custom Highlighting
+
This example shows how to create animated highlighting effects using the render prop.
+
{
+ if (!markTextAsSpoken || !boundary.word) {
+ return children
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ const text = typeof children === 'string' ? children : String(children)
+ const { startChar, endChar } = boundary
+ const before = text.substring(0, startChar)
+ const spoken = text.substring(startChar, endChar)
+ const after = text.substring(endChar)
+
+ return (
+ <>
+ {before}
+ {spoken}
+ {after}
+ >
+ )
+ }}
+ />
+
+ )
+}
+
+/**
+ * Border style render prop example
+ */
+const BorderStyleRenderProp: StoryFn = (args) => {
+ return (
+
+
Border Style Custom Highlighting
+
This example demonstrates custom border-based highlighting using the render prop.
+
{
+ if (!markTextAsSpoken || !boundary.word) {
+ return children
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ const text = typeof children === 'string' ? children : String(children)
+ const { startChar, endChar } = boundary
+ const before = text.substring(0, startChar)
+ const spoken = text.substring(startChar, endChar)
+ const after = text.substring(endChar)
+
+ return (
+ <>
+ {before}
+ {spoken}
+ {after}
+ >
+ )
+ }}
+ />
+
+ )
+}
+
+/**
+ * Complex content with render prop
+ */
+const ComplexContentRenderProp: StoryFn = (args) => {
+ return (
+
+
Complex Content with Custom Rendering
+
This example shows how the render prop works with more complex JSX content.
+
{
+ if (!markTextAsSpoken || !boundary.word) {
+ return children
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ const text = typeof children === 'string' ? children : String(children)
+ const { startChar, endChar } = boundary
+ const before = text.substring(0, startChar)
+ const spoken = text.substring(startChar, endChar)
+ const after = text.substring(endChar)
+
+ return (
+ <>
+ {before}
+
+ {spoken}
+
+ {after}
+ >
+ )
+ }}>
+
+
A Complex Example
+
This paragraph contains bold text and italic text to demonstrate how the render prop handles complex content.
+
+ - First list item with some text
+ - Second list item with more content
+
+
+
+
+ )
+}
+
+/**
+ * Conditional highlighting render prop
+ */
+const ConditionalRenderProp: StoryFn = (args) => {
+ return (
+
+
Conditional Custom Highlighting
+
This example demonstrates conditional highlighting based on word length or content.
+
{
+ if (!markTextAsSpoken || !boundary.word) {
+ return children
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ const text = typeof children === 'string' ? children : String(children)
+ const { startChar, endChar } = boundary
+ const before = text.substring(0, startChar)
+ const spoken = text.substring(startChar, endChar)
+ const after = text.substring(endChar)
+
+ // Apply different styles based on word length
+ const style = spoken.length > 5
+ ? {
+ backgroundColor: '#f44336',
+ color: 'white',
+ padding: '4px 8px',
+ borderRadius: '8px',
+ fontWeight: 'bold',
+ textTransform: 'uppercase' as const
+ }
+ : {
+ backgroundColor: '#2196f3',
+ color: 'white',
+ padding: '2px 4px',
+ borderRadius: '4px',
+ fontStyle: 'italic'
+ }
+
+ return (
+ <>
+ {before}
+ {spoken}
+ {after}
+ >
+ )
+ }}
+ />
+
+ )
+}
+
+/**
+ * Multi-color render prop example
+ */
+const MultiColorRenderProp: StoryFn = (args) => {
+ const colors = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#00bcd4', '#009688', '#4caf50']
+ let colorIndex = 0
+
+ return (
+
+
Multi-Color Custom Highlighting
+
This example cycles through different colors for each word that's spoken.
+
{
+ if (!markTextAsSpoken || !boundary.word) {
+ return children
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ const text = typeof children === 'string' ? children : String(children)
+ const { startChar, endChar } = boundary
+ const before = text.substring(0, startChar)
+ const spoken = text.substring(startChar, endChar)
+ const after = text.substring(endChar)
+
+ // Get the next color in the cycle
+ const color = colors[colorIndex % colors.length]
+ colorIndex++
+
+ return (
+ <>
+ {before}
+
+ {spoken}
+
+ {after}
+ >
+ )
+ }}
+ />
+
+ )
+}
+
+// Configure story arguments
+BasicRenderProp.args = {
+ markTextAsSpoken: true,
+ position: Positions.LC,
+ size: Sizes.MEDIUM
+}
+
+AnimatedRenderProp.args = {
+ markTextAsSpoken: true,
+ position: Positions.LC,
+ size: Sizes.MEDIUM
+}
+
+BorderStyleRenderProp.args = {
+ markTextAsSpoken: true,
+ position: Positions.LC,
+ size: Sizes.MEDIUM
+}
+
+ComplexContentRenderProp.args = {
+ markTextAsSpoken: true,
+ position: Positions.TC,
+ size: Sizes.MEDIUM
+}
+
+ConditionalRenderProp.args = {
+ markTextAsSpoken: true,
+ position: Positions.LC,
+ size: Sizes.MEDIUM
+}
+
+MultiColorRenderProp.args = {
+ markTextAsSpoken: true,
+ position: Positions.LC,
+ size: Sizes.MEDIUM
+}
+
+// Disable certain controls for render prop stories since they're handled by the render function
+const commonArgTypes = {
+ markColor: { control: false },
+ markBackgroundColor: { control: false },
+ render: { control: false },
+ markTextAsSpoken: { control: false }
+}
+
+BasicRenderProp.argTypes = commonArgTypes
+AnimatedRenderProp.argTypes = commonArgTypes
+BorderStyleRenderProp.argTypes = commonArgTypes
+ComplexContentRenderProp.argTypes = commonArgTypes
+ConditionalRenderProp.argTypes = commonArgTypes
+MultiColorRenderProp.argTypes = commonArgTypes
+
+export default {
+ title: 'tts-react/render-prop',
+ component: TextToSpeech,
+ parameters: {
+ docs: {
+ description: {
+ component: 'Examples demonstrating how to use the render prop feature for custom text highlighting during speech synthesis.'
+ }
+ }
+ }
+} as Meta
+
+export {
+ BasicRenderProp,
+ AnimatedRenderProp,
+ BorderStyleRenderProp,
+ ComplexContentRenderProp,
+ ConditionalRenderProp,
+ MultiColorRenderProp
+}
\ No newline at end of file