zds-pickers 4.2.0 → 4.2.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.
@@ -1,6 +1,35 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  const MEASUREMENT_ELEMENT_ID = '__react_svg_text_measurement_id';
4
+ /**
5
+ * The only computed properties that affect measured text geometry. Copying
6
+ * the full computed style (~300 properties) onto the measurement node per
7
+ * instance forced a style flush that dominated initial render in hosts
8
+ * with many SvgText instances.
9
+ */
10
+ const FONT_PROPERTIES = [
11
+ 'font-family',
12
+ 'font-feature-settings',
13
+ 'font-kerning',
14
+ 'font-size',
15
+ 'font-stretch',
16
+ 'font-style',
17
+ 'font-variant',
18
+ 'font-weight',
19
+ 'letter-spacing',
20
+ 'text-transform',
21
+ 'word-spacing',
22
+ ];
23
+ const fontSignature = (style) => FONT_PROPERTIES.map(key => style.getPropertyValue(key)).join('|');
24
+ /**
25
+ * Word widths only depend on the word and the font it renders in, so they
26
+ * are cached module-wide by font signature. Text that repeats across
27
+ * instances (labels, numbers) measures once per font, ever.
28
+ */
29
+ const fontMetricsCache = new Map();
30
+ const __resetFontMetricsCacheForTests = () => {
31
+ fontMetricsCache.clear();
32
+ };
4
33
  const calculateWordsByLines = (text, wordWidths, maxWidth, maxHeight) => {
5
34
  const { lineHeight, spaceWidth, wordsWithComputedWidth } = wordWidths;
6
35
  return text.split(/\s+/).reduce((result, word) => {
@@ -27,19 +56,39 @@ const calculateWordsByLines = (text, wordWidths, maxWidth, maxHeight) => {
27
56
  };
28
57
  const calculateWordWidths = (style, textNode, text) => {
29
58
  if (style && textNode) {
30
- // biome-ignore lint/complexity/noForEach: <explanation>
31
- Array.from(style).forEach(key => textNode.style.setProperty(key, style.getPropertyValue(key), style.getPropertyPriority(key)));
59
+ const signature = fontSignature(style);
60
+ let metrics = fontMetricsCache.get(signature);
32
61
  const wordArray = [...new Set(String(text).split(/\s+/))];
33
- const wordsWithComputedWidth = wordArray.reduce((wordMap, word) => {
34
- textNode.textContent = word;
35
- // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
36
- return { ...wordMap, [word]: textNode.getBBox().width };
37
- }, {});
38
- textNode.textContent = '\u00A0';
39
- const spaceWidth = textNode?.getComputedTextLength?.() || 8;
40
- const lineHeight = textNode.getBBox().height;
41
- textNode.setAttribute('style', '');
42
- return { wordsWithComputedWidth, spaceWidth, lineHeight };
62
+ const knownWidths = metrics?.widths;
63
+ const missingWords = knownWidths
64
+ ? wordArray.filter(word => !knownWidths.has(word))
65
+ : wordArray;
66
+ if (!metrics || missingWords.length) {
67
+ for (const key of FONT_PROPERTIES) {
68
+ textNode.style.setProperty(key, style.getPropertyValue(key), style.getPropertyPriority(key));
69
+ }
70
+ if (!metrics) {
71
+ textNode.textContent = '\u00A0';
72
+ const spaceWidth = textNode?.getComputedTextLength?.() || 8;
73
+ const lineHeight = textNode.getBBox().height;
74
+ metrics = { widths: new Map(), spaceWidth, lineHeight };
75
+ fontMetricsCache.set(signature, metrics);
76
+ }
77
+ for (const word of missingWords) {
78
+ textNode.textContent = word;
79
+ metrics.widths.set(word, textNode.getBBox().width);
80
+ }
81
+ textNode.setAttribute('style', '');
82
+ }
83
+ const wordsWithComputedWidth = {};
84
+ for (const word of wordArray) {
85
+ wordsWithComputedWidth[word] = metrics.widths.get(word) ?? 0;
86
+ }
87
+ return {
88
+ wordsWithComputedWidth,
89
+ spaceWidth: metrics.spaceWidth,
90
+ lineHeight: metrics.lineHeight,
91
+ };
43
92
  }
44
93
  return undefined;
45
94
  };
@@ -67,14 +116,28 @@ const SvgText = (props) => {
67
116
  if (el === null) {
68
117
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
69
118
  svg.setAttribute('id', MEASUREMENT_ELEMENT_ID);
119
+ svg.setAttribute('aria-hidden', 'true');
120
+ // Rendered but invisible: getBBox returns zeros under display:none,
121
+ // so park it offscreen instead. Being outside any host SVG also
122
+ // keeps per-word getBBox from forcing layout of live content.
123
+ svg.style.position = 'fixed';
124
+ svg.style.left = '-9999px';
125
+ svg.style.top = '0';
126
+ const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
127
+ svg.appendChild(textEl);
70
128
  document.body.appendChild(svg);
71
- svg.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'text'));
72
- measureRef.current = svg;
73
- return () => {
74
- document.body.removeChild(svg);
75
- };
129
+ // Measure against the <text> child — pointing at the <svg> container
130
+ // made every measurement return the svg's own box.
131
+ measureRef.current = textEl;
132
+ // Deliberately no cleanup: the element is a shared singleton and
133
+ // other mounted instances may still hold it as their measure target.
134
+ return;
76
135
  }
77
- measureRef.current = el;
136
+ // Hosts may provide either the <text> itself or a container around one.
137
+ measureRef.current =
138
+ el.tagName.toLowerCase() === 'text'
139
+ ? el
140
+ : (el.querySelector('text') ?? el);
78
141
  }, []);
79
142
  useEffect(() => {
80
143
  const wordWithWidths = calculateWordWidths(style, measureRef.current, text);
@@ -105,4 +168,4 @@ const SvgText = (props) => {
105
168
  // textAnchor={textAnchor}
106
169
  transform: transform, ...rest, children: _jsxs(_Fragment, { children: [displayedLines.map((line, idx) => (_jsx("tspan", { dx: x + dx, dy: idx === 0 ? startDy + dy : wordWidths?.lineHeight || 0, x: 0, children: line.words.join(' ') }, idx))), textLines.length && displayedLines.length !== textLines.length && (_jsxs(_Fragment, { children: [_jsx("tspan", { children: "..." }), _jsx("title", { children: text })] }))] }) }));
107
170
  };
108
- export { SvgText, MEASUREMENT_ELEMENT_ID };
171
+ export { __resetFontMetricsCacheForTests, calculateWordsByLines, calculateWordWidths, MEASUREMENT_ELEMENT_ID, SvgText, };
@@ -1,14 +1,17 @@
1
1
  declare const MEASUREMENT_ELEMENT_ID = "__react_svg_text_measurement_id";
2
+ declare const __resetFontMetricsCacheForTests: () => void;
2
3
  type WordsByLine = {
3
4
  words: string[];
4
5
  width: number;
5
6
  showLine: boolean;
6
7
  };
8
+ declare const calculateWordsByLines: (text: string, wordWidths: WordWidths, maxWidth: number, maxHeight: number) => WordsByLine[];
7
9
  type WordWidths = {
8
10
  wordsWithComputedWidth: Record<string, number>;
9
11
  spaceWidth: number;
10
12
  lineHeight: number;
11
13
  };
14
+ declare const calculateWordWidths: (style?: CSSStyleDeclaration, textNode?: SVGGraphicsElement | null, text?: string) => WordWidths | undefined;
12
15
  type TextProps = Partial<HTMLOrSVGElement> & {
13
16
  className?: string;
14
17
  dx?: number;
@@ -23,5 +26,5 @@ type TextProps = Partial<HTMLOrSVGElement> & {
23
26
  y?: number;
24
27
  };
25
28
  declare const SvgText: (props: TextProps) => import("react/jsx-runtime").JSX.Element;
26
- export { SvgText, MEASUREMENT_ELEMENT_ID };
29
+ export { __resetFontMetricsCacheForTests, calculateWordsByLines, calculateWordWidths, MEASUREMENT_ELEMENT_ID, SvgText, };
27
30
  export type { TextProps, WordWidths, WordsByLine };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zds-pickers",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/types/index.d.ts",