@momentum-design/components 0.101.4 → 0.102.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,454 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { html } from 'lit';
11
+ import { property, state } from 'lit/decorators.js';
12
+ import { ifDefined } from 'lit/directives/if-defined.js';
13
+ import { Component } from '../../models';
14
+ import '../text';
15
+ import styles from './typewriter.styles';
16
+ import { DEFAULTS, SPEED } from './typewriter.constants';
17
+ /**
18
+ * Typewriter component that creates a typewriter effect on text content.
19
+ * It uses the Text component internally, adding a progressive typing effect.
20
+ *
21
+ * The `type` attribute allows changing the text style (passed to the internal Text component).
22
+ * The `tagname` attribute allows changing the tag name of the text element (passed to the internal Text component).
23
+ * The default tag name is `p`.
24
+ *
25
+ * The `speed` attribute controls typing speed in milliseconds per character:
26
+ * - 'very-slow' = 240ms per character
27
+ * - 'slow' = 120ms per character
28
+ * - 'normal' = 60ms per character (default)
29
+ * - 'fast' = 20ms per character
30
+ * - 'very-fast' = 1ms per character
31
+ * - Or any numeric string representing milliseconds
32
+ *
33
+ * Advanced features:
34
+ * - Dynamic speed adjustment during typing
35
+ * - Chunked text addition via addTextChunk() method
36
+ * - Instant text addition via addInstantTextChunk() method or instant parameter
37
+ * - Mixed instant and animated chunks in queue
38
+ * - Continues typing in background tabs
39
+ * - Performance optimized for large text
40
+ * - maxQueueSize to limit memory usage from excessive queuing
41
+ * - event handling for typing completion and content changes
42
+ *
43
+ * The component includes accessibility features:
44
+ * - Screen readers announce the complete text, not character by character
45
+ * - Uses aria-live="polite" for dynamic content updates
46
+ * - Sets aria-busy during typing animation
47
+ *
48
+ * @dependency mdc-text
49
+ *
50
+ * @tagname mdc-typewriter
51
+ * @slot - Default slot for text content
52
+ *
53
+ * @csspart container - Container for the text element
54
+ * @csspart text - The text element (forwarded to mdc-text)
55
+ *
56
+ * @event typing-complete - (React: onTypingComplete) Fired when the typewriter finishes typing all content.
57
+ * Detail: \{ finalContent: string \}
58
+ * @event change - (React: onChange) Fired when the content of the typewriter changes.
59
+ * Detail: \{ content: string, isTyping: boolean \}
60
+ */
61
+ class Typewriter extends Component {
62
+ constructor() {
63
+ super(...arguments);
64
+ /**
65
+ * Specifies the text style to be applied to the internal text component.
66
+ * Uses the same types as the Text component.
67
+ * @default body-large-regular
68
+ */
69
+ this.type = DEFAULTS.TYPE;
70
+ /**
71
+ * Specifies the HTML tag name for the text element. The default tag name is `p`.
72
+ * This attribute is optional. When set, it changes the tag name of the internal text element.
73
+ *
74
+ * Acceptable values include all valid tag names from the Text component.
75
+ */
76
+ this.tagname = DEFAULTS.TEXT_ELEMENT_TAGNAME;
77
+ /**
78
+ * Speed of the typewriter effect in milliseconds per character.
79
+ * Can be a string preset or a numeric string in milliseconds.
80
+ * - 'very-slow' = 240ms per character
81
+ * - 'slow' = 120ms per character
82
+ * - 'normal' = 60ms per character (default)
83
+ * - 'fast' = 20ms per character
84
+ * - 'very-fast' = 1ms per character
85
+ * - Or any numeric string representing milliseconds (e.g., '100')
86
+ * @default 'normal' (60ms per character)
87
+ */
88
+ this.speed = DEFAULTS.SPEED;
89
+ /**
90
+ * Maximum number of text chunks that can be queued before oldest chunks are dropped.
91
+ * Set to prevent memory accumulation from excessive queuing.
92
+ * @default Number.MAX_SAFE_INTEGER (effectively unlimited)
93
+ */
94
+ this.maxQueueSize = Number.MAX_SAFE_INTEGER;
95
+ /**
96
+ * Internal state for the displayed text
97
+ * @internal
98
+ */
99
+ this.displayedText = '';
100
+ /**
101
+ * Internal state to track the original text
102
+ * @internal
103
+ */
104
+ this.originalText = '';
105
+ /**
106
+ * Character index for typing animation
107
+ * @internal
108
+ */
109
+ this.currentIndex = 0;
110
+ /**
111
+ * Typing animation timeout id
112
+ * @internal
113
+ */
114
+ this.typingTimeout = null;
115
+ /**
116
+ * Set of pending setTimeout IDs for cleanup
117
+ * @internal
118
+ */
119
+ this.pendingTimeouts = new Set();
120
+ /**
121
+ * Queue of text chunks to be added (limited to prevent memory issues)
122
+ * @internal
123
+ */
124
+ this.textChunkQueue = [];
125
+ /**
126
+ * Stores previous text content for comparison
127
+ * @internal
128
+ */
129
+ this.previousTextContent = '';
130
+ /**
131
+ * Whether the typing animation has completed
132
+ * @internal
133
+ */
134
+ this.typingComplete = true;
135
+ }
136
+ /**
137
+ * Called when the element is first connected to the document
138
+ */
139
+ connectedCallback() {
140
+ super.connectedCallback();
141
+ this.createTimeout(() => {
142
+ this.captureAndProcessContent();
143
+ }, 0);
144
+ }
145
+ /**
146
+ * Called when the element is disconnected from the document
147
+ */
148
+ disconnectedCallback() {
149
+ this.clearTypingAnimation();
150
+ this.clearAllTimeouts();
151
+ super.disconnectedCallback();
152
+ }
153
+ /**
154
+ * Helper to create tracked setTimeout that will be cleaned up on disconnect
155
+ */
156
+ createTimeout(callback, delay) {
157
+ const id = window.setTimeout(() => {
158
+ this.pendingTimeouts.delete(id);
159
+ callback();
160
+ }, delay);
161
+ this.pendingTimeouts.add(id);
162
+ return id;
163
+ }
164
+ /**
165
+ * Clear all pending timeouts
166
+ */
167
+ clearAllTimeouts() {
168
+ this.pendingTimeouts.forEach(id => clearTimeout(id));
169
+ this.pendingTimeouts.clear();
170
+ }
171
+ /**
172
+ * Clears the text chunk queue and stops processing
173
+ * Useful for resetting the component state
174
+ */
175
+ clearQueue() {
176
+ this.textChunkQueue.length = 0;
177
+ }
178
+ /**
179
+ * Adds a chunk of text to be typed out, optionally with a different speed
180
+ * @param text - The text to add
181
+ * @param speed - Optional speed override for this chunk
182
+ * @param instant - If true, text appears instantly without animation
183
+ */
184
+ addTextChunk(text, speed, instant) {
185
+ if (!text)
186
+ return;
187
+ if (this.maxQueueSize < Number.MAX_SAFE_INTEGER && this.textChunkQueue.length >= this.maxQueueSize) {
188
+ this.textChunkQueue.splice(0, this.textChunkQueue.length - this.maxQueueSize + 1);
189
+ }
190
+ this.textChunkQueue.push({ text, speed, instant });
191
+ this.processChunkQueue();
192
+ }
193
+ /**
194
+ * Adds a chunk of text instantly without typing animation
195
+ * @param text - The text to add instantly
196
+ */
197
+ addInstantTextChunk(text) {
198
+ this.addTextChunk(text, undefined, true);
199
+ }
200
+ /**
201
+ * Processes all chunks in the queue
202
+ */
203
+ processChunkQueue() {
204
+ // Process the next chunk - the callback chain will handle remaining chunks
205
+ if (this.textChunkQueue.length > 0) {
206
+ this.processNextChunk();
207
+ }
208
+ }
209
+ /**
210
+ * Processes the next chunk in the queue
211
+ */
212
+ processNextChunk() {
213
+ if (this.textChunkQueue.length === 0) {
214
+ return;
215
+ }
216
+ const chunk = this.textChunkQueue.shift();
217
+ const originalSpeed = this.speed;
218
+ if (chunk.speed !== undefined) {
219
+ this.speed = chunk.speed;
220
+ }
221
+ this.originalText += chunk.text;
222
+ this.typingComplete = false;
223
+ if (chunk.instant) {
224
+ this.displayedText = this.originalText;
225
+ this.currentIndex = this.originalText.length;
226
+ this.typingComplete = true;
227
+ this.requestUpdate();
228
+ if (chunk.speed !== undefined) {
229
+ this.speed = originalSpeed;
230
+ }
231
+ if (this.textChunkQueue.length > 0) {
232
+ this.createTimeout(() => {
233
+ this.processNextChunk();
234
+ }, 0);
235
+ }
236
+ else {
237
+ this.dispatchEvent(new CustomEvent(DEFAULTS.CUSTOM_EVENT.TYPING_COMPLETE, {
238
+ bubbles: true,
239
+ composed: true,
240
+ }));
241
+ }
242
+ }
243
+ else {
244
+ this.startTypingAnimation(() => {
245
+ if (chunk.speed !== undefined) {
246
+ this.speed = originalSpeed;
247
+ }
248
+ // Continue processing remaining chunks or fire completion event
249
+ if (this.textChunkQueue.length > 0) {
250
+ // Use tracked timeout to avoid deep recursion and ensure clean state
251
+ this.createTimeout(() => {
252
+ this.processNextChunk();
253
+ }, 0);
254
+ }
255
+ else {
256
+ this.dispatchEvent(new CustomEvent(DEFAULTS.CUSTOM_EVENT.TYPING_COMPLETE, {
257
+ bubbles: true,
258
+ composed: true,
259
+ }));
260
+ }
261
+ });
262
+ }
263
+ }
264
+ /**
265
+ * Gets the typing delay in milliseconds per character
266
+ */
267
+ getTypingDelayMs() {
268
+ const speedValue = this.speed;
269
+ // Handle preset string values
270
+ switch (speedValue) {
271
+ case 'slow':
272
+ return SPEED.SLOW;
273
+ case 'fast':
274
+ return SPEED.FAST;
275
+ case 'very-slow':
276
+ return SPEED.VERY_SLOW;
277
+ case 'very-fast':
278
+ return SPEED.VERY_FAST;
279
+ case 'normal':
280
+ return SPEED.NORMAL;
281
+ default: {
282
+ // Try to parse as a number string, fallback to normal speed
283
+ const numericSpeed = parseInt(speedValue, 10);
284
+ return !Number.isNaN(numericSpeed) ? Math.max(10, numericSpeed) : SPEED.NORMAL;
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Responds to property changes
290
+ */
291
+ updated(changedProperties) {
292
+ super.updated(changedProperties);
293
+ // Only restart animation if speed changed during active typing
294
+ // and we're not in the middle of chunk processing
295
+ if (changedProperties.has('speed') && !this.typingComplete && this.textChunkQueue.length === 0) {
296
+ this.startTypingAnimation();
297
+ }
298
+ }
299
+ /**
300
+ * Captures slot content and starts typewriter animation
301
+ */
302
+ captureAndProcessContent() {
303
+ var _a;
304
+ const slot = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('slot');
305
+ if (!slot)
306
+ return;
307
+ const content = slot
308
+ .assignedNodes()
309
+ .filter(node => node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE)
310
+ .map(node => node.textContent || '')
311
+ .join('');
312
+ // If no change in content, don't restart animation
313
+ if (content === this.previousTextContent)
314
+ return;
315
+ // If content is completely different, reset animation
316
+ if (this.displayedText === '' || !content.includes(this.displayedText)) {
317
+ this.originalText = content;
318
+ this.displayedText = '';
319
+ this.currentIndex = 0;
320
+ this.typingComplete = false;
321
+ }
322
+ else {
323
+ // For additional content, only type the new part
324
+ this.originalText = content;
325
+ this.typingComplete = false;
326
+ }
327
+ this.dispatchEvent(new CustomEvent('change', {
328
+ bubbles: true,
329
+ composed: true,
330
+ detail: {
331
+ content: this.originalText,
332
+ isTyping: !this.typingComplete,
333
+ },
334
+ }));
335
+ this.previousTextContent = content;
336
+ this.startTypingAnimation();
337
+ }
338
+ /**
339
+ * Starts the typewriter animation
340
+ */
341
+ startTypingAnimation(onComplete) {
342
+ this.clearTypingAnimation();
343
+ // Don't start animation if there's no new content to type
344
+ if (this.displayedText === this.originalText) {
345
+ this.typingComplete = true;
346
+ // IMPORTANT: Always call onComplete even if no animation is needed
347
+ if (onComplete) {
348
+ this.createTimeout(() => {
349
+ onComplete();
350
+ }, 0);
351
+ }
352
+ return;
353
+ }
354
+ // Make sure currentIndex is in sync with displayedText
355
+ this.currentIndex = this.displayedText.length;
356
+ this.typingComplete = false;
357
+ const typeNextCharacter = () => {
358
+ if (this.currentIndex < this.originalText.length) {
359
+ const nextChar = this.originalText[this.currentIndex];
360
+ const newText = this.displayedText + nextChar;
361
+ if (newText !== this.displayedText) {
362
+ this.displayedText = newText;
363
+ this.requestUpdate();
364
+ }
365
+ this.currentIndex += 1;
366
+ // Schedule next character
367
+ this.typingTimeout = window.setTimeout(typeNextCharacter, this.getTypingDelayMs());
368
+ }
369
+ else {
370
+ this.clearTypingAnimation();
371
+ this.typingComplete = true;
372
+ if (onComplete) {
373
+ this.createTimeout(() => {
374
+ onComplete();
375
+ }, 0);
376
+ }
377
+ else {
378
+ this.createTimeout(() => {
379
+ this.dispatchEvent(new CustomEvent(DEFAULTS.CUSTOM_EVENT.TYPING_COMPLETE, {
380
+ bubbles: true,
381
+ composed: true,
382
+ detail: {
383
+ finalContent: this.originalText,
384
+ },
385
+ }));
386
+ }, 0);
387
+ }
388
+ }
389
+ };
390
+ this.typingTimeout = window.setTimeout(typeNextCharacter, this.getTypingDelayMs());
391
+ }
392
+ /**
393
+ * Clears the typing animation timeout
394
+ */
395
+ clearTypingAnimation() {
396
+ if (this.typingTimeout !== null) {
397
+ clearTimeout(this.typingTimeout);
398
+ this.typingTimeout = null;
399
+ }
400
+ }
401
+ /**
402
+ * Handler for slotchange event
403
+ */
404
+ handleSlotChange() {
405
+ this.captureAndProcessContent();
406
+ }
407
+ /**
408
+ * Render method that uses mdc-text component internally with accessibility features
409
+ */
410
+ render() {
411
+ return html `
412
+ <div part="${DEFAULTS.CSS_PART_CONTAINER}" aria-live="polite" aria-busy="${!this.typingComplete}">
413
+ <mdc-text
414
+ part="${DEFAULTS.CSS_PART_TEXT}"
415
+ type="${this.type}"
416
+ tagname="${ifDefined(this.tagname)}"
417
+ aria-label="${this.originalText}"
418
+ >${this.displayedText}</mdc-text
419
+ >
420
+ <slot @slotchange=${this.handleSlotChange} class="typewriter-hidden"></slot>
421
+ </div>
422
+ `;
423
+ }
424
+ }
425
+ Typewriter.styles = [...Component.styles, ...styles];
426
+ __decorate([
427
+ property({ attribute: 'type', reflect: true, type: String }),
428
+ __metadata("design:type", String)
429
+ ], Typewriter.prototype, "type", void 0);
430
+ __decorate([
431
+ property({ attribute: 'tagname', reflect: true, type: String }),
432
+ __metadata("design:type", String)
433
+ ], Typewriter.prototype, "tagname", void 0);
434
+ __decorate([
435
+ property({ attribute: 'speed', reflect: true }),
436
+ __metadata("design:type", String)
437
+ ], Typewriter.prototype, "speed", void 0);
438
+ __decorate([
439
+ property({ attribute: 'max-queue-size', type: Number, reflect: true }),
440
+ __metadata("design:type", Number)
441
+ ], Typewriter.prototype, "maxQueueSize", void 0);
442
+ __decorate([
443
+ state(),
444
+ __metadata("design:type", String)
445
+ ], Typewriter.prototype, "displayedText", void 0);
446
+ __decorate([
447
+ state(),
448
+ __metadata("design:type", String)
449
+ ], Typewriter.prototype, "originalText", void 0);
450
+ __decorate([
451
+ state(),
452
+ __metadata("design:type", Boolean)
453
+ ], Typewriter.prototype, "typingComplete", void 0);
454
+ export default Typewriter;
@@ -0,0 +1,20 @@
1
+ declare const TAG_NAME: "mdc-typewriter";
2
+ declare const SPEED: {
3
+ readonly VERY_SLOW: 240;
4
+ readonly SLOW: 120;
5
+ readonly NORMAL: 60;
6
+ readonly FAST: 20;
7
+ readonly VERY_FAST: 1;
8
+ };
9
+ declare const DEFAULTS: {
10
+ readonly TYPE: "body-large-regular";
11
+ readonly TEXT_ELEMENT_TAGNAME: "p";
12
+ readonly CSS_PART_TEXT: "text";
13
+ readonly CSS_PART_CONTAINER: "container";
14
+ readonly CHILDREN: "";
15
+ readonly SPEED: "normal";
16
+ readonly CUSTOM_EVENT: {
17
+ readonly TYPING_COMPLETE: "typing-complete";
18
+ };
19
+ };
20
+ export { TAG_NAME, DEFAULTS, SPEED };
@@ -0,0 +1,22 @@
1
+ import utils from '../../utils/tag-name';
2
+ import { DEFAULTS as TEXT_DEFAULTS } from '../text/text.constants';
3
+ const TAG_NAME = utils.constructTagName('typewriter');
4
+ const SPEED = {
5
+ VERY_SLOW: 240,
6
+ SLOW: 120,
7
+ NORMAL: 60,
8
+ FAST: 20,
9
+ VERY_FAST: 1,
10
+ };
11
+ const DEFAULTS = {
12
+ TYPE: TEXT_DEFAULTS.TYPE,
13
+ TEXT_ELEMENT_TAGNAME: TEXT_DEFAULTS.TEXT_ELEMENT_TAGNAME,
14
+ CSS_PART_TEXT: 'text',
15
+ CSS_PART_CONTAINER: 'container',
16
+ CHILDREN: '',
17
+ SPEED: 'normal',
18
+ CUSTOM_EVENT: {
19
+ TYPING_COMPLETE: 'typing-complete',
20
+ },
21
+ };
22
+ export { TAG_NAME, DEFAULTS, SPEED };
@@ -0,0 +1,2 @@
1
+ declare const styles: import("lit").CSSResult[];
2
+ export default styles;
@@ -0,0 +1,19 @@
1
+ import { css } from 'lit';
2
+ const styles = [
3
+ css `
4
+ :host {
5
+ display: block;
6
+ position: relative;
7
+ }
8
+
9
+ [part='container'] {
10
+ display: flex;
11
+ align-items: baseline;
12
+ }
13
+
14
+ .typewriter-hidden {
15
+ display: none;
16
+ }
17
+ `,
18
+ ];
19
+ export default styles;
@@ -0,0 +1,21 @@
1
+ import type { TypedEvent } from '../../utils/types';
2
+ import type { TextType, TagName } from '../text/text.types';
3
+ import type Typewriter from './typewriter.component';
4
+ type TypewriterSpeed = 'slow' | 'normal' | 'fast' | 'very-slow' | 'very-fast' | string;
5
+ interface TextChunk {
6
+ text: string;
7
+ speed?: TypewriterSpeed;
8
+ instant?: boolean;
9
+ }
10
+ type TypewriterChangeEvent = TypedEvent<Typewriter, {
11
+ content: string;
12
+ isTyping: boolean;
13
+ }>;
14
+ type TypewriterTypingCompleteEvent = TypedEvent<Typewriter, {
15
+ finalContent: string;
16
+ }>;
17
+ interface Events {
18
+ onChangeEvent: TypewriterChangeEvent;
19
+ onTypingCompleteEvent: TypewriterTypingCompleteEvent;
20
+ }
21
+ export type { TypewriterSpeed, TextType, TagName, Events, TextChunk, TypewriterChangeEvent, TypewriterTypingCompleteEvent, };
@@ -0,0 +1 @@
1
+ export {};