UNPKG

@dialpad/dialtone-vue

Version:

Vue component library for Dialpad's design system Dialtone

1 lines 17.2 kB
{"version":3,"file":"motion-text.cjs","sources":["../../../recipes/motion/motion_text/motion_text.vue"],"sourcesContent":["<template>\n <span\n ref=\"contentRef\"\n :class=\"motionTextClasses\"\n :style=\"componentStyles\"\n :data-text-content=\"isStaticAnimationMode ? text : undefined\"\n :aria-live=\"isAnimating ? 'polite' : 'off'\"\n :aria-label=\"screenReaderText || undefined\"\n >\n <!-- Screen reader content -->\n <span\n v-if=\"screenReaderText\"\n class=\"dt-recipe-motion-text__sr-only\"\n >\n {{ screenReaderText }}\n </span>\n\n <!-- Gradient-sweep and shimmer modes: Simple static text with gradient animation -->\n <template v-if=\"isStaticAnimationMode\">\n {{ text }}\n <slot v-if=\"!text\" />\n </template>\n\n <!-- Character-by-character animated content for other modes -->\n <span\n v-else\n :key=\"animationKey\"\n class=\"dt-recipe-motion-text__content\"\n :aria-hidden=\"isAnimating\"\n >\n <template\n v-for=\"(word, wordIdx) in words\"\n >\n <Transition\n :key=\"`${animationKey}-${wordIdx}`\"\n :name=\"`dt-recipe-motion-text-word-${animationMode}`\"\n >\n <span\n v-if=\"wordIdx < visibleWordCount\"\n class=\"dt-recipe-motion-text__word\"\n :data-text-content=\"word.text\"\n :style=\"{ '--word-index': wordIdx }\"\n >\n <template\n v-for=\"(char, charIdx) in word.chars\"\n >\n <Transition\n :key=\"`${animationKey}-${wordIdx}-${charIdx}`\"\n :name=\"`dt-recipe-motion-text-char-${animationMode}`\"\n >\n <span\n v-if=\"charIdx < visibleCharsPerWord[wordIdx]\"\n class=\"dt-recipe-motion-text__char\"\n :style=\"{\n '--char-index': charIdx,\n '--char-delay': `${charIdx * timing.characterDelay}ms`,\n }\"\n >{{ char }}</span>\n </Transition>\n </template>\n </span>\n </Transition>\n </template>\n </span>\n\n <!-- Fallback slot content -->\n <span\n v-if=\"!words.length && !text && !isStaticAnimationMode\"\n class=\"dt-recipe-motion-text__fallback\"\n >\n <slot />\n </span>\n </span>\n</template>\n\n<script>\nimport { MOTION_TEXT_ANIMATION_MODES, MOTION_TEXT_SPEEDS, MOTION_TEXT_TIMING_PRESETS } from './motion_text_constants';\n\nexport default {\n compatConfig: { MODE: 3 },\n name: 'DtRecipeMotionText',\n\n inheritAttrs: false,\n\n props: {\n /**\n * The text content to animate.\n * @type {string}\n */\n text: {\n type: String,\n default: '',\n },\n\n /**\n * The animation mode to use for the text reveal.\n * @values gradient-in, fade-in, slide-in, gradient-sweep, shimmer, none\n */\n animationMode: {\n type: String,\n default: 'gradient-in',\n validator: (value) => MOTION_TEXT_ANIMATION_MODES.includes(value),\n },\n\n /**\n * Animation speed using t-shirt sizing.\n * @values sm, md, lg\n */\n speed: {\n type: String,\n default: 'md',\n validator: (value) => MOTION_TEXT_SPEEDS.includes(value),\n },\n\n /**\n * Whether to start animation automatically when component is mounted.\n * @values true, false\n */\n autoStart: {\n type: Boolean,\n default: true,\n },\n\n /**\n * Whether to loop the animation continuously.\n * @values true, false\n */\n loop: {\n type: Boolean,\n default: false,\n },\n\n /**\n * Whether to respect the user's prefers-reduced-motion system setting.\n * @values true, false\n */\n respectsReducedMotion: {\n type: Boolean,\n default: true,\n },\n\n /**\n * Alternative text for screen readers. If provided, this will be announced\n * instead of the animated text.\n * @type {string}\n */\n screenReaderText: {\n type: String,\n default: '',\n },\n },\n\n emits: [\n /**\n * Emitted when the animation starts.\n * @event start\n */\n 'start',\n\n /**\n * Emitted when the animation completes.\n * @event complete\n */\n 'complete',\n\n /**\n * Emitted during animation progress.\n * @event progress\n * @type {{ wordsComplete: number, totalWords: number, progress: number }}\n */\n 'progress',\n\n /**\n * Emitted when the animation is paused.\n * @event pause\n */\n 'pause',\n\n /**\n * Emitted when the animation resumes.\n * @event resume\n */\n 'resume',\n ],\n\n data () {\n return {\n words: [],\n visibleWordCount: 0,\n visibleCharsPerWord: [],\n isAnimating: false,\n isPaused: false,\n isLooped: false,\n animationTimeouts: [],\n prefersReducedMotion: false,\n animationKey: 0,\n };\n },\n\n computed: {\n /**\n * Get timing preset based on speed prop\n */\n timing () {\n return MOTION_TEXT_TIMING_PRESETS[this.speed];\n },\n\n /**\n * Computed styles with timing CSS variables\n */\n componentStyles () {\n return {\n '--dt-recipe-motion-text-duration': `${this.timing.duration}ms`,\n '--dt-recipe-motion-text-char-duration': `${this.timing.duration}ms`,\n '--dt-recipe-motion-text-word-duration': `${this.timing.duration * 2}ms`,\n };\n },\n\n /**\n * Check if current animation mode is static (gradient-sweep or shimmer)\n */\n isStaticAnimationMode () {\n return this.animationMode === 'gradient-sweep' || this.animationMode === 'shimmer';\n },\n\n /**\n * Computed classes for the motion text element\n */\n motionTextClasses () {\n return [\n 'dt-recipe-motion-text',\n `dt-recipe-motion-text--${this.animationMode}`,\n {\n 'dt-recipe-motion-text--animating': this.isAnimating,\n 'dt-recipe-motion-text--paused': this.isPaused,\n 'dt-recipe-motion-text--looped': this.isLooped,\n },\n this.$attrs.class,\n ];\n },\n },\n\n watch: {\n text () {\n this.reset();\n this.initializeContent();\n },\n\n loop: {\n handler (newVal) {\n this.isLooped = newVal;\n },\n\n immediate: true,\n },\n },\n\n mounted () {\n this.checkReducedMotion();\n this.initializeContent();\n },\n\n beforeUnmount () {\n this.clearTimeouts();\n },\n\n methods: {\n /**\n * Self-contained text processing from DOM nodes\n */\n processTextToChars (node) {\n const words = [];\n\n const processNode = (node, index = 0) => {\n if (node.nodeType === Node.TEXT_NODE) {\n const matches = node.textContent?.match(/\\S+\\s*/g) || [];\n words.push(...matches.map((text, i) => ({\n text,\n chars: text.split(''),\n index: index + i,\n })));\n return index + matches.length;\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n let currentIdx = index;\n Array.from(node.childNodes).forEach(child => {\n currentIdx = processNode(child, currentIdx);\n });\n return currentIdx;\n }\n return index;\n };\n\n processNode(node);\n return words;\n },\n\n /**\n * Process direct text prop into word/character data\n */\n processDirectText (text) {\n if (!text) return [];\n\n const matches = text.match(/\\S+\\s*/g) || [];\n return matches.map((wordText, i) => ({\n text: wordText,\n chars: wordText.split(''),\n index: i,\n }));\n },\n\n /**\n * Check for reduced motion preference\n */\n checkReducedMotion () {\n if (typeof window !== 'undefined' && window.matchMedia) {\n this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n }\n },\n\n /**\n * Clear all animation timeouts\n */\n clearTimeouts () {\n this.animationTimeouts.forEach(timeout => clearTimeout(timeout));\n this.animationTimeouts = [];\n },\n\n /**\n * Start the animation\n * @public\n */\n start () {\n if (this.isAnimating) return;\n\n this.isAnimating = true;\n this.isPaused = false;\n this.$emit('start');\n\n // Skip animation if reduced motion is preferred and enabled\n if (this.respectsReducedMotion && this.prefersReducedMotion) {\n this.showAllContent();\n return;\n }\n\n if (this.animationMode === 'none') {\n this.showAllContent();\n return;\n }\n\n // For gradient-sweep and shimmer modes, just mark as animating (CSS handles the animation)\n if (this.isStaticAnimationMode) {\n return;\n }\n\n // Start the word-by-word animation for \"-in\" modes\n this.showNextWord();\n },\n\n /**\n * Pause the animation\n * @public\n */\n pause () {\n if (!this.isAnimating || this.isPaused) return;\n\n this.isPaused = true;\n this.clearTimeouts();\n this.$emit('pause');\n },\n\n /**\n * Resume the animation\n * @public\n */\n resume () {\n if (!this.isPaused) return;\n\n this.isPaused = false;\n this.$emit('resume');\n this.showNextWord();\n },\n\n /**\n * Reset the animation to initial state\n * @public\n */\n reset () {\n this.clearTimeouts();\n this.isAnimating = false;\n this.isPaused = false;\n this.visibleWordCount = 0;\n this.visibleCharsPerWord = Array(this.words.length).fill(0);\n this.animationKey++;\n },\n\n /**\n * Skip to the end of the animation\n * @public\n */\n skipToEnd () {\n this.showAllContent();\n },\n\n /**\n * Show all content immediately\n */\n showAllContent () {\n this.visibleWordCount = this.words.length;\n this.visibleCharsPerWord = this.words.map(word => word.chars.length);\n setTimeout(() => {\n this.isAnimating = false;\n this.$emit('complete');\n }, 0);\n },\n\n /**\n * Show next word in sequence\n */\n showNextWord () {\n if (this.isPaused || this.visibleWordCount >= this.words.length) {\n if (this.visibleWordCount >= this.words.length) {\n this.completeAnimation();\n }\n return;\n }\n\n const timeout = setTimeout(() => {\n this.visibleWordCount++;\n this.$emit('progress', {\n wordsComplete: this.visibleWordCount,\n totalWords: this.words.length,\n progress: this.visibleWordCount / this.words.length,\n });\n\n this.animateCharsForWord(this.visibleWordCount - 1);\n }, this.timing.wordDelay);\n\n this.animationTimeouts.push(timeout);\n },\n\n /**\n * Animate characters for a specific word\n */\n animateCharsForWord (wordIdx) {\n if (this.isPaused || wordIdx >= this.words.length) return;\n\n this.visibleCharsPerWord[wordIdx] = 0;\n const chars = this.words[wordIdx].chars.length;\n\n const revealChar = () => {\n if (this.isPaused || this.visibleCharsPerWord[wordIdx] >= chars) {\n if (this.visibleCharsPerWord[wordIdx] >= chars) {\n this.showNextWord();\n }\n return;\n }\n\n this.visibleCharsPerWord[wordIdx]++;\n const timeout = setTimeout(revealChar, this.timing.characterDelay);\n this.animationTimeouts.push(timeout);\n };\n\n revealChar();\n },\n\n /**\n * Complete the animation\n */\n completeAnimation () {\n this.isAnimating = false;\n this.clearTimeouts();\n\n this.$emit('complete');\n\n if (this.loop) {\n const timeout = setTimeout(() => {\n this.reset();\n this.$nextTick(() => {\n this.start();\n });\n }, 500);\n\n this.animationTimeouts.push(timeout);\n }\n },\n\n /**\n * Initialize content based on text prop or slot content\n */\n initializeContent () {\n // For gradient-sweep and shimmer modes, skip word/character processing\n if (this.isStaticAnimationMode) {\n if (this.autoStart) {\n this.$nextTick(() => this.start());\n }\n return;\n }\n\n if (this.text) {\n this.words = this.processDirectText(this.text);\n } else if (this.$refs.contentRef) {\n this.words = this.processTextToChars(this.$refs.contentRef);\n }\n\n this.visibleCharsPerWord = Array(this.words.length).fill(0);\n this.visibleWordCount = 0;\n\n if (this.autoStart && this.words.length > 0) {\n this.$nextTick(() => this.start());\n }\n },\n },\n};\n</script>\n"],"names":["_sfc_main","value","MOTION_TEXT_ANIMATION_MODES","MOTION_TEXT_SPEEDS","MOTION_TEXT_TIMING_PRESETS","newVal","node","words","processNode","index","matches","_a","text","i","currentIdx","child","wordText","timeout","word","wordIdx","chars","revealChar"],"mappings":"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"}