fetta 1.0.2 → 1.1.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # fetta
1
+ # Fetta
2
2
 
3
3
  A text-splitting library with kerning compensation for smooth, natural text animations.
4
4
 
@@ -14,7 +14,7 @@ Split text into characters, words, and lines while preserving the original typog
14
14
  - **Auto-Revert** — Restore original HTML after animations
15
15
  - **Masking** — Wrap elements in clip containers for reveal animations
16
16
  - **Emoji Support** — Properly handles compound emojis and complex Unicode characters
17
- - **Accessible** — Adds `aria-label` with original text for screen readers
17
+ - **Accessible** — Automatic screen reader support, even when splitting text with nested links or emphasis
18
18
  - **TypeScript** — Full type definitions included
19
19
  - **React Component** — Declarative wrapper for React projects
20
20
  - **Built-in InView** — Viewport detection for scroll-triggered animations in React
@@ -255,7 +255,7 @@ Each element also receives a `data-index` attribute with its position.
255
255
 
256
256
  - **Fonts must be loaded** before splitting. The React component waits for `document.fonts.ready` automatically.
257
257
  - **Ligatures are disabled** (`font-variant-ligatures: none`) because ligatures cannot span multiple elements.
258
- - **Accessibility**: Split elements receive an `aria-label` with the original text for screen readers.
258
+ - **Accessibility**: Automatic screen reader support for both simple text and text with nested elements like links.
259
259
 
260
260
  ## Browser Support
261
261
 
@@ -64,6 +64,32 @@ var INLINE_ELEMENTS = /* @__PURE__ */ new Set([
64
64
  "var"
65
65
  ]);
66
66
  var isSafari = typeof navigator !== "undefined" && /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
67
+ var srOnlyStylesInjected = false;
68
+ function injectSrOnlyStyles() {
69
+ if (srOnlyStylesInjected || typeof document === "undefined") return;
70
+ const style = document.createElement("style");
71
+ style.textContent = `
72
+ .fetta-sr-only {
73
+ position: absolute;
74
+ width: 1px;
75
+ height: 1px;
76
+ padding: 0;
77
+ margin: -1px;
78
+ overflow: hidden;
79
+ clip-path: inset(50%);
80
+ white-space: nowrap;
81
+ border-width: 0;
82
+ }`;
83
+ document.head.appendChild(style);
84
+ srOnlyStylesInjected = true;
85
+ }
86
+ function createScreenReaderCopy(originalHTML) {
87
+ const srCopy = document.createElement("span");
88
+ srCopy.className = "fetta-sr-only";
89
+ srCopy.innerHTML = originalHTML;
90
+ srCopy.dataset.fettaSrCopy = "true";
91
+ return srCopy;
92
+ }
67
93
  function hasInlineDescendants(element) {
68
94
  const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT);
69
95
  let node;
@@ -236,6 +262,9 @@ function createSpan(className, index, display = "inline-block", options) {
236
262
  if (options == null ? void 0 : options.willChange) {
237
263
  span.style.willChange = "transform, opacity";
238
264
  }
265
+ if (options == null ? void 0 : options.ariaHidden) {
266
+ span.setAttribute("aria-hidden", "true");
267
+ }
239
268
  return span;
240
269
  }
241
270
  function createMaskWrapper(display = "inline-block") {
@@ -283,7 +312,8 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
283
312
  const wordSpan = createSpan(wordClass, wordIndex, "inline-block", {
284
313
  propIndex: options == null ? void 0 : options.propIndex,
285
314
  willChange: options == null ? void 0 : options.willChange,
286
- propName: "word"
315
+ propName: "word",
316
+ ariaHidden: options == null ? void 0 : options.ariaHidden
287
317
  });
288
318
  if (measuredWord.noSpaceBefore) {
289
319
  noSpaceBeforeSet.add(wordSpan);
@@ -295,7 +325,8 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
295
325
  const charSpan = createSpan(charClass, globalCharIndex, "inline-block", {
296
326
  propIndex: options == null ? void 0 : options.propIndex,
297
327
  willChange: options == null ? void 0 : options.willChange,
298
- propName: "char"
328
+ propName: "char",
329
+ ariaHidden: options == null ? void 0 : options.ariaHidden
299
330
  });
300
331
  charSpan.textContent = measuredChar.char;
301
332
  globalCharIndex++;
@@ -456,7 +487,8 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
456
487
  const lineSpan = createSpan(lineClass, lineIndex, "block", {
457
488
  propIndex: options == null ? void 0 : options.propIndex,
458
489
  willChange: options == null ? void 0 : options.willChange,
459
- propName: "line"
490
+ propName: "line",
491
+ ariaHidden: options == null ? void 0 : options.ariaHidden
460
492
  });
461
493
  allLines.push(lineSpan);
462
494
  let wi = 0;
@@ -631,7 +663,6 @@ function splitText(element, {
631
663
  let currentChars = [];
632
664
  let currentWords = [];
633
665
  let currentLines = [];
634
- element.setAttribute("aria-label", text);
635
666
  if (splitChars) {
636
667
  element.style.fontVariantLigatures = "none";
637
668
  }
@@ -650,11 +681,24 @@ function splitText(element, {
650
681
  splitChars,
651
682
  splitWords,
652
683
  splitLines,
653
- { propIndex, willChange, mask }
684
+ { propIndex, willChange, mask, ariaHidden: !trackAncestors }
654
685
  );
655
686
  currentChars = chars;
656
687
  currentWords = words;
657
688
  currentLines = lines;
689
+ if (trackAncestors) {
690
+ injectSrOnlyStyles();
691
+ const visualWrapper = document.createElement("span");
692
+ visualWrapper.setAttribute("aria-hidden", "true");
693
+ visualWrapper.dataset.fettaVisual = "true";
694
+ while (element.firstChild) {
695
+ visualWrapper.appendChild(element.firstChild);
696
+ }
697
+ element.appendChild(visualWrapper);
698
+ element.appendChild(createScreenReaderCopy(originalHTML));
699
+ } else {
700
+ element.setAttribute("aria-label", text);
701
+ }
658
702
  const dispose = () => {
659
703
  if (!isActive) return;
660
704
  if (resizeObserver) {
@@ -670,7 +714,9 @@ function splitText(element, {
670
714
  const revert = () => {
671
715
  if (!isActive) return;
672
716
  element.innerHTML = originalHTML;
673
- element.removeAttribute("aria-label");
717
+ if (!trackAncestors) {
718
+ element.removeAttribute("aria-label");
719
+ }
674
720
  if (splitChars) {
675
721
  element.style.fontVariantLigatures = "none";
676
722
  }
@@ -713,11 +759,21 @@ function splitText(element, {
713
759
  splitChars,
714
760
  splitWords,
715
761
  splitLines,
716
- { propIndex, willChange, mask }
762
+ { propIndex, willChange, mask, ariaHidden: !trackAncestors }
717
763
  );
718
764
  currentChars = result.chars;
719
765
  currentWords = result.words;
720
766
  currentLines = result.lines;
767
+ if (trackAncestors) {
768
+ const visualWrapper = document.createElement("span");
769
+ visualWrapper.setAttribute("aria-hidden", "true");
770
+ visualWrapper.dataset.fettaVisual = "true";
771
+ while (element.firstChild) {
772
+ visualWrapper.appendChild(element.firstChild);
773
+ }
774
+ element.appendChild(visualWrapper);
775
+ element.appendChild(createScreenReaderCopy(originalHTML));
776
+ }
721
777
  const newFingerprint = getLineFingerprint(result.lines);
722
778
  if (onResize && newFingerprint !== previousFingerprint) {
723
779
  onResize({
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { splitText } from './chunk-G5P33GJE.js';
1
+ export { splitText } from './chunk-6EZFFMJ3.js';
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-G5P33GJE.js';
1
+ import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-6EZFFMJ3.js';
2
2
  import { forwardRef, useRef, useCallback, useState, useLayoutEffect, useEffect, isValidElement, cloneElement } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetta",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "Text splitting library with kerning compensation for animations",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,8 +34,23 @@
34
34
  "test:ui": "vitest --ui",
35
35
  "test:coverage": "vitest run --coverage",
36
36
  "test:e2e": "playwright test",
37
- "test:all": "vitest run --coverage && playwright test"
37
+ "test:all": "vitest run --coverage && playwright test",
38
+ "size": "size-limit",
39
+ "size:why": "size-limit --why"
38
40
  },
41
+ "size-limit": [
42
+ {
43
+ "name": "Core",
44
+ "path": "dist/index.js",
45
+ "import": "*"
46
+ },
47
+ {
48
+ "name": "React",
49
+ "path": "dist/react.js",
50
+ "import": "*",
51
+ "ignore": ["react"]
52
+ }
53
+ ],
39
54
  "peerDependencies": {
40
55
  "react": ">=18.0.0"
41
56
  },
@@ -46,6 +61,7 @@
46
61
  },
47
62
  "devDependencies": {
48
63
  "@playwright/test": "^1.49.0",
64
+ "@size-limit/preset-small-lib": "^12.0.0",
49
65
  "@testing-library/dom": "^10.4.0",
50
66
  "@testing-library/jest-dom": "^6.6.3",
51
67
  "@testing-library/react": "^16.1.0",
@@ -56,6 +72,7 @@
56
72
  "react": "^19.2.3",
57
73
  "react-dom": "^19.0.0",
58
74
  "serve": "^14.2.4",
75
+ "size-limit": "^12.0.0",
59
76
  "tsup": "^8.0.0",
60
77
  "typescript": "^5",
61
78
  "vitest": "^2.1.8"