@wordpress/block-editor 15.6.2 → 15.6.4

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.
Files changed (43) hide show
  1. package/build/components/block-settings-menu/block-settings-dropdown.js +4 -1
  2. package/build/components/block-settings-menu/block-settings-dropdown.js.map +2 -2
  3. package/build/components/border-radius-control/utils.js +7 -3
  4. package/build/components/border-radius-control/utils.js.map +2 -2
  5. package/build/components/global-styles/border-panel.js +11 -7
  6. package/build/components/global-styles/border-panel.js.map +2 -2
  7. package/build/components/rich-text/index.js +1 -0
  8. package/build/components/rich-text/index.js.map +2 -2
  9. package/build/hooks/block-bindings.js +6 -2
  10. package/build/hooks/block-bindings.js.map +2 -2
  11. package/build/hooks/fit-text.js +45 -8
  12. package/build/hooks/fit-text.js.map +2 -2
  13. package/build/utils/fit-text-frontend.js +28 -37
  14. package/build/utils/fit-text-frontend.js.map +2 -2
  15. package/build/utils/fit-text-utils.js +7 -14
  16. package/build/utils/fit-text-utils.js.map +2 -2
  17. package/build-module/components/block-settings-menu/block-settings-dropdown.js +4 -1
  18. package/build-module/components/block-settings-menu/block-settings-dropdown.js.map +2 -2
  19. package/build-module/components/border-radius-control/utils.js +7 -3
  20. package/build-module/components/border-radius-control/utils.js.map +2 -2
  21. package/build-module/components/global-styles/border-panel.js +11 -7
  22. package/build-module/components/global-styles/border-panel.js.map +2 -2
  23. package/build-module/components/rich-text/index.js +1 -0
  24. package/build-module/components/rich-text/index.js.map +2 -2
  25. package/build-module/hooks/block-bindings.js +6 -2
  26. package/build-module/hooks/block-bindings.js.map +2 -2
  27. package/build-module/hooks/fit-text.js +45 -8
  28. package/build-module/hooks/fit-text.js.map +2 -2
  29. package/build-module/utils/fit-text-frontend.js +28 -37
  30. package/build-module/utils/fit-text-frontend.js.map +2 -2
  31. package/build-module/utils/fit-text-utils.js +7 -14
  32. package/build-module/utils/fit-text-utils.js.map +2 -2
  33. package/package.json +7 -6
  34. package/src/components/block-settings-menu/block-settings-dropdown.js +4 -1
  35. package/src/components/border-radius-control/test/utils.js +90 -0
  36. package/src/components/border-radius-control/utils.js +7 -3
  37. package/src/components/global-styles/border-panel.js +11 -7
  38. package/src/components/rich-text/index.js +1 -0
  39. package/src/hooks/block-bindings.js +6 -2
  40. package/src/hooks/fit-text.js +63 -12
  41. package/src/utils/fit-text-frontend.js +41 -73
  42. package/src/utils/fit-text-utils.js +12 -29
  43. package/tsconfig.json +1 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/fit-text-frontend.js"],
4
- "sourcesContent": ["/**\n * Frontend fit text functionality.\n * Automatically detects and initializes fit text on blocks with the has-fit-text class.\n */\n\n/**\n * Internal dependencies\n */\nimport { optimizeFitText } from './fit-text-utils';\n\n/**\n * Counter for generating unique element IDs.\n */\nlet idCounter = 0;\n\n/**\n * Get or create a unique style element for a fit text element.\n *\n * @param {string} elementId Unique identifier for the element.\n * @return {HTMLElement} Style element.\n */\nfunction getOrCreateStyleElement( elementId ) {\n\tconst styleId = `fit-text-${ elementId }`;\n\tlet styleElement = document.getElementById( styleId );\n\tif ( ! styleElement ) {\n\t\tstyleElement = document.createElement( 'style' );\n\t\tstyleElement.id = styleId;\n\t\tdocument.head.appendChild( styleElement );\n\t}\n\treturn styleElement;\n}\n\n/**\n * Generate a unique identifier for a fit text element.\n *\n * @param {HTMLElement} element The element to identify.\n * @return {string} Unique identifier.\n */\nfunction getElementIdentifier( element ) {\n\tif ( ! element.dataset.fitTextId ) {\n\t\telement.dataset.fitTextId = `fit-text-${ ++idCounter }`;\n\t}\n\treturn element.dataset.fitTextId;\n}\n\n/**\n * Initialize fit text functionality for a single element.\n *\n * @param {HTMLElement} element Element with fit text enabled.\n */\nfunction initializeFitText( element ) {\n\tconst elementId = getElementIdentifier( element );\n\n\tconst applyFitText = () => {\n\t\tconst styleElement = getOrCreateStyleElement( elementId );\n\t\tconst elementSelector = `[data-fit-text-id=\\\"${ elementId }\\\"]`;\n\n\t\t// Style management callback\n\t\tconst applyStylesFn = ( css ) => {\n\t\t\tstyleElement.textContent = css;\n\t\t};\n\n\t\toptimizeFitText( element, elementSelector, applyStylesFn );\n\t};\n\n\t// Initial sizing\n\tapplyFitText();\n\n\t// Watch for parent container resize\n\tif ( window.ResizeObserver && element.parentElement ) {\n\t\tconst resizeObserver = new window.ResizeObserver( applyFitText );\n\t\tresizeObserver.observe( element.parentElement );\n\t}\n}\n\n/**\n * Initialize fit text on all elements with the has-fit-text class.\n */\nfunction initializeAllFitText() {\n\tconst elements = document.querySelectorAll( '.has-fit-text' );\n\telements.forEach( initializeFitText );\n}\n\nwindow.addEventListener( 'load', initializeAllFitText );\n"],
5
- "mappings": "AAQA,SAAS,uBAAuB;AAKhC,IAAI,YAAY;AAQhB,SAAS,wBAAyB,WAAY;AAC7C,QAAM,UAAU,YAAa,SAAU;AACvC,MAAI,eAAe,SAAS,eAAgB,OAAQ;AACpD,MAAK,CAAE,cAAe;AACrB,mBAAe,SAAS,cAAe,OAAQ;AAC/C,iBAAa,KAAK;AAClB,aAAS,KAAK,YAAa,YAAa;AAAA,EACzC;AACA,SAAO;AACR;AAQA,SAAS,qBAAsB,SAAU;AACxC,MAAK,CAAE,QAAQ,QAAQ,WAAY;AAClC,YAAQ,QAAQ,YAAY,YAAa,EAAE,SAAU;AAAA,EACtD;AACA,SAAO,QAAQ,QAAQ;AACxB;AAOA,SAAS,kBAAmB,SAAU;AACrC,QAAM,YAAY,qBAAsB,OAAQ;AAEhD,QAAM,eAAe,MAAM;AAC1B,UAAM,eAAe,wBAAyB,SAAU;AACxD,UAAM,kBAAkB,sBAAwB,SAAU;AAG1D,UAAM,gBAAgB,CAAE,QAAS;AAChC,mBAAa,cAAc;AAAA,IAC5B;AAEA,oBAAiB,SAAS,iBAAiB,aAAc;AAAA,EAC1D;AAGA,eAAa;AAGb,MAAK,OAAO,kBAAkB,QAAQ,eAAgB;AACrD,UAAM,iBAAiB,IAAI,OAAO,eAAgB,YAAa;AAC/D,mBAAe,QAAS,QAAQ,aAAc;AAAA,EAC/C;AACD;AAKA,SAAS,uBAAuB;AAC/B,QAAM,WAAW,SAAS,iBAAkB,eAAgB;AAC5D,WAAS,QAAS,iBAAkB;AACrC;AAEA,OAAO,iBAAkB,QAAQ,oBAAqB;",
4
+ "sourcesContent": ["/**\n * Frontend fit text functionality.\n * Automatically detects and initializes fit text on blocks with the has-fit-text class.\n * Supports both initial page load and Interactivity API client-side navigation.\n */\n\n/**\n * WordPress dependencies\n */\nimport { store, getElement, getContext } from '@wordpress/interactivity';\n\n/**\n * Internal dependencies\n */\nimport { optimizeFitText } from './fit-text-utils';\n\n// Initialize via Interactivity API for client-side navigation\nstore( 'core/fit-text', {\n\tcallbacks: {\n\t\tinit() {\n\t\t\tconst context = getContext();\n\t\t\tconst { ref } = getElement();\n\n\t\t\tconst applyFontSize = ( fontSize ) => {\n\t\t\t\tif ( fontSize === 0 ) {\n\t\t\t\t\tref.style.fontSize = '';\n\t\t\t\t} else {\n\t\t\t\t\tref.style.fontSize = `${ fontSize }px`;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Initial fit text optimization.\n\t\t\tcontext.fontSize = optimizeFitText( ref, applyFontSize );\n\n\t\t\t// Starts ResizeObserver to handle dynamic resizing.\n\t\t\tif ( window.ResizeObserver && ref.parentElement ) {\n\t\t\t\tconst resizeObserver = new window.ResizeObserver( () => {\n\t\t\t\t\tcontext.fontSize = optimizeFitText( ref, applyFontSize );\n\t\t\t\t} );\n\t\t\t\tresizeObserver.observe( ref.parentElement );\n\t\t\t\tresizeObserver.observe( ref );\n\n\t\t\t\t// Return cleanup function to be called when element is removed.\n\t\t\t\treturn () => {\n\t\t\t\t\tif ( resizeObserver ) {\n\t\t\t\t\t\tresizeObserver.disconnect();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t},\n} );\n"],
5
+ "mappings": "AASA,SAAS,OAAO,YAAY,kBAAkB;AAK9C,SAAS,uBAAuB;AAGhC,MAAO,iBAAiB;AAAA,EACvB,WAAW;AAAA,IACV,OAAO;AACN,YAAM,UAAU,WAAW;AAC3B,YAAM,EAAE,IAAI,IAAI,WAAW;AAE3B,YAAM,gBAAgB,CAAE,aAAc;AACrC,YAAK,aAAa,GAAI;AACrB,cAAI,MAAM,WAAW;AAAA,QACtB,OAAO;AACN,cAAI,MAAM,WAAW,GAAI,QAAS;AAAA,QACnC;AAAA,MACD;AAGA,cAAQ,WAAW,gBAAiB,KAAK,aAAc;AAGvD,UAAK,OAAO,kBAAkB,IAAI,eAAgB;AACjD,cAAM,iBAAiB,IAAI,OAAO,eAAgB,MAAM;AACvD,kBAAQ,WAAW,gBAAiB,KAAK,aAAc;AAAA,QACxD,CAAE;AACF,uBAAe,QAAS,IAAI,aAAc;AAC1C,uBAAe,QAAS,GAAI;AAG5B,eAAO,MAAM;AACZ,cAAK,gBAAiB;AACrB,2BAAe,WAAW;AAAA,UAC3B;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD,CAAE;",
6
6
  "names": []
7
7
  }
@@ -1,14 +1,11 @@
1
- function generateCSSRule(elementSelector, fontSize) {
2
- return `${elementSelector} { font-size: ${fontSize}px !important; }`;
3
- }
4
- function findOptimalFontSize(textElement, elementSelector, applyStylesFn) {
1
+ function findOptimalFontSize(textElement, applyFontSize) {
5
2
  const alreadyHasScrollableHeight = textElement.scrollHeight > textElement.clientHeight;
6
3
  let minSize = 5;
7
4
  let maxSize = 600;
8
5
  let bestSize = minSize;
9
6
  while (minSize <= maxSize) {
10
7
  const midSize = Math.floor((minSize + maxSize) / 2);
11
- applyStylesFn(generateCSSRule(elementSelector, midSize));
8
+ applyFontSize(midSize);
12
9
  const fitsWidth = textElement.scrollWidth <= textElement.clientWidth;
13
10
  const fitsHeight = alreadyHasScrollableHeight || textElement.scrollHeight <= textElement.clientHeight;
14
11
  if (fitsWidth && fitsHeight) {
@@ -20,18 +17,14 @@ function findOptimalFontSize(textElement, elementSelector, applyStylesFn) {
20
17
  }
21
18
  return bestSize;
22
19
  }
23
- function optimizeFitText(textElement, elementSelector, applyStylesFn) {
20
+ function optimizeFitText(textElement, applyFontSize) {
24
21
  if (!textElement) {
25
22
  return;
26
23
  }
27
- applyStylesFn("");
28
- const optimalSize = findOptimalFontSize(
29
- textElement,
30
- elementSelector,
31
- applyStylesFn
32
- );
33
- const cssRule = generateCSSRule(elementSelector, optimalSize);
34
- applyStylesFn(cssRule);
24
+ applyFontSize(0);
25
+ const optimalSize = findOptimalFontSize(textElement, applyFontSize);
26
+ applyFontSize(optimalSize);
27
+ return optimalSize;
35
28
  }
36
29
  export {
37
30
  optimizeFitText
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/fit-text-utils.js"],
4
- "sourcesContent": ["/**\n * Shared utility functions for fit text functionality.\n * Uses callback-based approach for maximum code reuse between editor and frontend.\n */\n\n/**\n * Generate CSS rule for single text element.\n *\n * @param {string} elementSelector CSS selector for the text element\n * @param {number} fontSize Font size in pixels\n * @return {string} CSS rule string\n */\nfunction generateCSSRule( elementSelector, fontSize ) {\n\treturn `${ elementSelector } { font-size: ${ fontSize }px !important; }`;\n}\n\n/**\n * Find optimal font size using simple binary search between 5-600px.\n *\n * @param {HTMLElement} textElement The text element\n * @param {string} elementSelector CSS selector for the text element\n * @param {Function} applyStylesFn Function to apply test styles\n * @return {number} Optimal font size\n */\nfunction findOptimalFontSize( textElement, elementSelector, applyStylesFn ) {\n\tconst alreadyHasScrollableHeight =\n\t\ttextElement.scrollHeight > textElement.clientHeight;\n\tlet minSize = 5;\n\tlet maxSize = 600;\n\tlet bestSize = minSize;\n\n\twhile ( minSize <= maxSize ) {\n\t\tconst midSize = Math.floor( ( minSize + maxSize ) / 2 );\n\t\tapplyStylesFn( generateCSSRule( elementSelector, midSize ) );\n\n\t\tconst fitsWidth = textElement.scrollWidth <= textElement.clientWidth;\n\t\tconst fitsHeight =\n\t\t\talreadyHasScrollableHeight ||\n\t\t\ttextElement.scrollHeight <= textElement.clientHeight;\n\n\t\tif ( fitsWidth && fitsHeight ) {\n\t\t\tbestSize = midSize;\n\t\t\tminSize = midSize + 1;\n\t\t} else {\n\t\t\tmaxSize = midSize - 1;\n\t\t}\n\t}\n\n\treturn bestSize;\n}\n\n/**\n * Complete fit text optimization for a single text element.\n * Handles the full flow using callbacks for style management.\n *\n * @param {HTMLElement} textElement The text element (paragraph, heading, etc.)\n * @param {string} elementSelector CSS selector for the text element\n * @param {Function} applyStylesFn Function to apply CSS styles (pass empty string to clear)\n */\nexport function optimizeFitText( textElement, elementSelector, applyStylesFn ) {\n\tif ( ! textElement ) {\n\t\treturn;\n\t}\n\n\tapplyStylesFn( '' );\n\n\tconst optimalSize = findOptimalFontSize(\n\t\ttextElement,\n\t\telementSelector,\n\t\tapplyStylesFn\n\t);\n\n\tconst cssRule = generateCSSRule( elementSelector, optimalSize );\n\tapplyStylesFn( cssRule );\n}\n"],
5
- "mappings": "AAYA,SAAS,gBAAiB,iBAAiB,UAAW;AACrD,SAAO,GAAI,eAAgB,iBAAkB,QAAS;AACvD;AAUA,SAAS,oBAAqB,aAAa,iBAAiB,eAAgB;AAC3E,QAAM,6BACL,YAAY,eAAe,YAAY;AACxC,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,WAAW;AAEf,SAAQ,WAAW,SAAU;AAC5B,UAAM,UAAU,KAAK,OAAS,UAAU,WAAY,CAAE;AACtD,kBAAe,gBAAiB,iBAAiB,OAAQ,CAAE;AAE3D,UAAM,YAAY,YAAY,eAAe,YAAY;AACzD,UAAM,aACL,8BACA,YAAY,gBAAgB,YAAY;AAEzC,QAAK,aAAa,YAAa;AAC9B,iBAAW;AACX,gBAAU,UAAU;AAAA,IACrB,OAAO;AACN,gBAAU,UAAU;AAAA,IACrB;AAAA,EACD;AAEA,SAAO;AACR;AAUO,SAAS,gBAAiB,aAAa,iBAAiB,eAAgB;AAC9E,MAAK,CAAE,aAAc;AACpB;AAAA,EACD;AAEA,gBAAe,EAAG;AAElB,QAAM,cAAc;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAEA,QAAM,UAAU,gBAAiB,iBAAiB,WAAY;AAC9D,gBAAe,OAAQ;AACxB;",
4
+ "sourcesContent": ["/**\n * Shared utility functions for fit text functionality.\n * Uses callback-based approach for maximum code reuse between editor and frontend.\n */\n\n/**\n * Find optimal font size using simple binary search between 5-600px.\n *\n * @param {HTMLElement} textElement The text element\n * @param {Function} applyFontSize Function that receives font size in pixels\n * @return {number} Optimal font size\n */\nfunction findOptimalFontSize( textElement, applyFontSize ) {\n\tconst alreadyHasScrollableHeight =\n\t\ttextElement.scrollHeight > textElement.clientHeight;\n\tlet minSize = 5;\n\tlet maxSize = 600;\n\tlet bestSize = minSize;\n\n\twhile ( minSize <= maxSize ) {\n\t\tconst midSize = Math.floor( ( minSize + maxSize ) / 2 );\n\t\tapplyFontSize( midSize );\n\n\t\tconst fitsWidth = textElement.scrollWidth <= textElement.clientWidth;\n\t\tconst fitsHeight =\n\t\t\talreadyHasScrollableHeight ||\n\t\t\ttextElement.scrollHeight <= textElement.clientHeight;\n\n\t\tif ( fitsWidth && fitsHeight ) {\n\t\t\tbestSize = midSize;\n\t\t\tminSize = midSize + 1;\n\t\t} else {\n\t\t\tmaxSize = midSize - 1;\n\t\t}\n\t}\n\n\treturn bestSize;\n}\n\n/**\n * Complete fit text optimization for a single text element.\n * Handles the full flow using callbacks for font size application.\n *\n * @param {HTMLElement} textElement The text element (paragraph, heading, etc.)\n * @param {Function} applyFontSize Function that receives font size in pixels (0 to clear, >0 to apply)\n */\nexport function optimizeFitText( textElement, applyFontSize ) {\n\tif ( ! textElement ) {\n\t\treturn;\n\t}\n\n\tapplyFontSize( 0 );\n\n\tconst optimalSize = findOptimalFontSize( textElement, applyFontSize );\n\n\tapplyFontSize( optimalSize );\n\treturn optimalSize;\n}\n"],
5
+ "mappings": "AAYA,SAAS,oBAAqB,aAAa,eAAgB;AAC1D,QAAM,6BACL,YAAY,eAAe,YAAY;AACxC,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,WAAW;AAEf,SAAQ,WAAW,SAAU;AAC5B,UAAM,UAAU,KAAK,OAAS,UAAU,WAAY,CAAE;AACtD,kBAAe,OAAQ;AAEvB,UAAM,YAAY,YAAY,eAAe,YAAY;AACzD,UAAM,aACL,8BACA,YAAY,gBAAgB,YAAY;AAEzC,QAAK,aAAa,YAAa;AAC9B,iBAAW;AACX,gBAAU,UAAU;AAAA,IACrB,OAAO;AACN,gBAAU,UAAU;AAAA,IACrB;AAAA,EACD;AAEA,SAAO;AACR;AASO,SAAS,gBAAiB,aAAa,eAAgB;AAC7D,MAAK,CAAE,aAAc;AACpB;AAAA,EACD;AAEA,gBAAe,CAAE;AAEjB,QAAM,cAAc,oBAAqB,aAAa,aAAc;AAEpE,gBAAe,WAAY;AAC3B,SAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/block-editor",
3
- "version": "15.6.2",
3
+ "version": "15.6.4",
4
4
  "description": "Generic block editor.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -62,8 +62,8 @@
62
62
  "@wordpress/blob": "^4.33.1",
63
63
  "@wordpress/block-serialization-default-parser": "^5.33.1",
64
64
  "@wordpress/blocks": "^15.6.1",
65
- "@wordpress/commands": "^1.33.1",
66
- "@wordpress/components": "^30.6.1",
65
+ "@wordpress/commands": "^1.33.2",
66
+ "@wordpress/components": "^30.6.2",
67
67
  "@wordpress/compose": "^7.33.1",
68
68
  "@wordpress/data": "^10.33.1",
69
69
  "@wordpress/date": "^5.33.1",
@@ -75,17 +75,18 @@
75
75
  "@wordpress/html-entities": "^4.33.1",
76
76
  "@wordpress/i18n": "^6.6.1",
77
77
  "@wordpress/icons": "^11.0.1",
78
+ "@wordpress/interactivity": "^6.33.1",
78
79
  "@wordpress/is-shallow-equal": "^5.33.1",
79
80
  "@wordpress/keyboard-shortcuts": "^5.33.1",
80
81
  "@wordpress/keycodes": "^4.33.1",
81
82
  "@wordpress/notices": "^5.33.1",
82
- "@wordpress/preferences": "^4.33.1",
83
+ "@wordpress/preferences": "^4.33.2",
83
84
  "@wordpress/priority-queue": "^3.33.1",
84
85
  "@wordpress/private-apis": "^1.33.1",
85
86
  "@wordpress/rich-text": "^7.33.1",
86
87
  "@wordpress/style-engine": "^2.33.1",
87
88
  "@wordpress/token-list": "^3.33.1",
88
- "@wordpress/upload-media": "^0.18.1",
89
+ "@wordpress/upload-media": "^0.18.2",
89
90
  "@wordpress/url": "^4.33.1",
90
91
  "@wordpress/warning": "^3.33.1",
91
92
  "@wordpress/wordcount": "^4.33.1",
@@ -111,5 +112,5 @@
111
112
  "publishConfig": {
112
113
  "access": "public"
113
114
  },
114
- "gitHead": "45005cc254bab59182927e35a68cd22f6320634d"
115
+ "gitHead": "2e2a11a11f0c5c9cb2bba2dd40b8046d2a10dc9d"
115
116
  }
@@ -336,7 +336,10 @@ export function BlockSettingsDropdown( {
336
336
  ) }
337
337
  { count === 1 && (
338
338
  <CommentIconSlotFill.Slot
339
- fillProps={ { onClose } }
339
+ fillProps={ {
340
+ clientId: firstBlockClientId,
341
+ onClose,
342
+ } }
340
343
  />
341
344
  ) }
342
345
  </MenuGroup>
@@ -107,6 +107,96 @@ describe( 'getAllValue', () => {
107
107
  expect( getAllValue( undefined ) ).toBe( undefined );
108
108
  } );
109
109
  } );
110
+
111
+ describe( 'when provided complex CSS values (clamp, min, max, calc)', () => {
112
+ it( 'should preserve clamp values when all corners have the same clamp value', () => {
113
+ const clampValue = 'clamp(1rem, 2vw, 3rem)';
114
+ const values = {
115
+ bottomLeft: clampValue,
116
+ bottomRight: clampValue,
117
+ topLeft: clampValue,
118
+ topRight: clampValue,
119
+ };
120
+ expect( getAllValue( values ) ).toBe( clampValue );
121
+ } );
122
+
123
+ it( 'should preserve min() values when all corners have the same min value', () => {
124
+ const minValue = 'min(10px, 5vw)';
125
+ const values = {
126
+ bottomLeft: minValue,
127
+ bottomRight: minValue,
128
+ topLeft: minValue,
129
+ topRight: minValue,
130
+ };
131
+ expect( getAllValue( values ) ).toBe( minValue );
132
+ } );
133
+
134
+ it( 'should preserve max() values when all corners have the same max value', () => {
135
+ const maxValue = 'max(20px, 10vw)';
136
+ const values = {
137
+ bottomLeft: maxValue,
138
+ bottomRight: maxValue,
139
+ topLeft: maxValue,
140
+ topRight: maxValue,
141
+ };
142
+ expect( getAllValue( values ) ).toBe( maxValue );
143
+ } );
144
+
145
+ it( 'should preserve calc() values when all corners have the same calc value', () => {
146
+ const calcValue = 'calc(100% - 20px)';
147
+ const values = {
148
+ bottomLeft: calcValue,
149
+ bottomRight: calcValue,
150
+ topLeft: calcValue,
151
+ topRight: calcValue,
152
+ };
153
+ expect( getAllValue( values ) ).toBe( calcValue );
154
+ } );
155
+
156
+ it( 'should return undefined when complex CSS values are mixed', () => {
157
+ const values = {
158
+ bottomLeft: 'clamp(1rem, 2vw, 3rem)',
159
+ bottomRight: 'clamp(1rem, 2vw, 3rem)',
160
+ topLeft: 'min(10px, 5vw)',
161
+ topRight: 'clamp(1rem, 2vw, 3rem)',
162
+ };
163
+ expect( getAllValue( values ) ).toBe( undefined );
164
+ } );
165
+
166
+ it( 'should return undefined when complex CSS values are mixed with simple values', () => {
167
+ const values = {
168
+ bottomLeft: 'clamp(1rem, 2vw, 3rem)',
169
+ bottomRight: 'clamp(1rem, 2vw, 3rem)',
170
+ topLeft: '2px',
171
+ topRight: 'clamp(1rem, 2vw, 3rem)',
172
+ };
173
+ expect( getAllValue( values ) ).toBe( undefined );
174
+ } );
175
+
176
+ it( 'should preserve string values that cannot be parsed at all (no numeric prefix)', () => {
177
+ // Values with no numeric prefix cannot be parsed, so they should be preserved
178
+ const unparseableValue = 'apples';
179
+ const values = {
180
+ bottomLeft: unparseableValue,
181
+ bottomRight: unparseableValue,
182
+ topLeft: unparseableValue,
183
+ topRight: unparseableValue,
184
+ };
185
+ expect( getAllValue( values ) ).toBe( unparseableValue );
186
+ } );
187
+
188
+ it( 'should parse numeric prefix from partially parseable values', () => {
189
+ // Values with numeric prefix get parsed, so "32apples" becomes "32"
190
+ const partiallyParseableValue = '32apples';
191
+ const values = {
192
+ bottomLeft: partiallyParseableValue,
193
+ bottomRight: partiallyParseableValue,
194
+ topLeft: partiallyParseableValue,
195
+ topRight: partiallyParseableValue,
196
+ };
197
+ expect( getAllValue( values ) ).toBe( '32' );
198
+ } );
199
+ } );
110
200
  } );
111
201
 
112
202
  describe( 'hasMixedValues', () => {
@@ -60,9 +60,13 @@ export function getAllValue( values = {} ) {
60
60
  return values;
61
61
  }
62
62
 
63
- const parsedQuantitiesAndUnits = Object.values( values ).map( ( value ) =>
64
- parseQuantityAndUnitFromRawValue( value )
65
- );
63
+ const parsedQuantitiesAndUnits = Object.values( values ).map( ( value ) => {
64
+ const newValue = parseQuantityAndUnitFromRawValue( value );
65
+ if ( typeof value === 'string' && newValue[ 0 ] === undefined ) {
66
+ return [ value, '' ];
67
+ }
68
+ return newValue;
69
+ } );
66
70
 
67
71
  const allValues = parsedQuantitiesAndUnits.map(
68
72
  ( value ) => value[ 0 ] ?? ''
@@ -146,17 +146,21 @@ export default function BorderPanel( {
146
146
  // Border radius.
147
147
  const showBorderRadius = useHasBorderRadiusControl( settings );
148
148
  const borderRadiusValues = useMemo( () => {
149
- if ( typeof border?.radius !== 'object' ) {
150
- return border?.radius;
149
+ if ( typeof inheritedValue?.border?.radius !== 'object' ) {
150
+ return decodeValue( inheritedValue?.border?.radius );
151
151
  }
152
152
 
153
153
  return {
154
- topLeft: border?.radius?.topLeft,
155
- topRight: border?.radius?.topRight,
156
- bottomLeft: border?.radius?.bottomLeft,
157
- bottomRight: border?.radius?.bottomRight,
154
+ topLeft: decodeValue( inheritedValue?.border?.radius?.topLeft ),
155
+ topRight: decodeValue( inheritedValue?.border?.radius?.topRight ),
156
+ bottomLeft: decodeValue(
157
+ inheritedValue?.border?.radius?.bottomLeft
158
+ ),
159
+ bottomRight: decodeValue(
160
+ inheritedValue?.border?.radius?.bottomRight
161
+ ),
158
162
  };
159
- }, [ border?.radius ] );
163
+ }, [ inheritedValue?.border?.radius, decodeValue ] );
160
164
  const setBorderRadius = ( newBorderRadius ) =>
161
165
  setBorder( { ...border, radius: newBorderRadius } );
162
166
  const hasBorderRadius = () => {
@@ -569,6 +569,7 @@ const PublicForwardedRichTextContainer = forwardRef( ( props, ref ) => {
569
569
  } = removeNativeProps( props );
570
570
  return (
571
571
  <Tag
572
+ ref={ ref }
572
573
  { ...contentProps }
573
574
  dangerouslySetInnerHTML={ {
574
575
  __html: valueToHTMLString( value, multiline ),
@@ -449,7 +449,11 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
449
449
  export default {
450
450
  edit: BlockBindingsPanel,
451
451
  attributeKeys: [ 'metadata' ],
452
- hasSupport() {
453
- return true;
452
+ hasSupport( name ) {
453
+ return ! [
454
+ 'core/post-date',
455
+ 'core/navigation-link',
456
+ 'core/navigation-submenu',
457
+ ].includes( name );
454
458
  },
455
459
  };
@@ -11,6 +11,8 @@ import {
11
11
  __experimentalToolsPanelItem as ToolsPanelItem,
12
12
  } from '@wordpress/components';
13
13
 
14
+ const EMPTY_OBJECT = {};
15
+
14
16
  /**
15
17
  * Internal dependencies
16
18
  */
@@ -62,14 +64,19 @@ function useFitText( { fitText, name, clientId } ) {
62
64
  const hasFitTextSupport = hasBlockSupport( name, FIT_TEXT_SUPPORT_KEY );
63
65
  const blockElement = useBlockElement( clientId );
64
66
 
65
- // Monitor block attribute changes
66
- // Any attribute may change the available space.
67
- const blockAttributes = useSelect(
67
+ // Monitor block attribute changes, and parent changes.
68
+ // Any attribute or parent change may change the available space.
69
+ const { blockAttributes, parentId } = useSelect(
68
70
  ( select ) => {
69
71
  if ( ! clientId || ! hasFitTextSupport || ! fitText ) {
70
- return;
72
+ return EMPTY_OBJECT;
71
73
  }
72
- return select( blockEditorStore ).getBlockAttributes( clientId );
74
+ return {
75
+ blockAttributes:
76
+ select( blockEditorStore ).getBlockAttributes( clientId ),
77
+ parentId:
78
+ select( blockEditorStore ).getBlockRootClientId( clientId ),
79
+ };
73
80
  },
74
81
  [ clientId, hasFitTextSupport, fitText ]
75
82
  );
@@ -90,11 +97,15 @@ function useFitText( { fitText, name, clientId } ) {
90
97
 
91
98
  const blockSelector = `#block-${ clientId }`;
92
99
 
93
- const applyStylesFn = ( css ) => {
94
- styleElement.textContent = css;
100
+ const applyFontSize = ( fontSize ) => {
101
+ if ( fontSize === 0 ) {
102
+ styleElement.textContent = '';
103
+ } else {
104
+ styleElement.textContent = `${ blockSelector } { font-size: ${ fontSize }px !important; }`;
105
+ }
95
106
  };
96
107
 
97
- optimizeFitText( blockElement, blockSelector, applyStylesFn );
108
+ optimizeFitText( blockElement, applyFontSize );
98
109
  }, [ blockElement, clientId, hasFitTextSupport, fitText ] );
99
110
 
100
111
  useEffect( () => {
@@ -107,21 +118,54 @@ function useFitText( { fitText, name, clientId } ) {
107
118
  return;
108
119
  }
109
120
 
110
- // Apply initially
111
- applyFitText();
112
-
113
121
  // Store current element value for cleanup
114
122
  const currentElement = blockElement;
123
+ const previousVisibility = currentElement.style.visibility;
124
+
125
+ // Store IDs for cleanup
126
+ let hideFrameId = null;
127
+ let calculateFrameId = null;
128
+ let showTimeoutId = null;
129
+
130
+ // We are hiding the element doing the calculation of fit text
131
+ // and then showing it again to avoid the user noticing a flash of potentially
132
+ // big fitText while the binary search is happening.
133
+ hideFrameId = window.requestAnimationFrame( () => {
134
+ currentElement.style.visibility = 'hidden';
135
+ // Wait for browser to render the hidden state
136
+ calculateFrameId = window.requestAnimationFrame( () => {
137
+ applyFitText();
138
+
139
+ // Using a timeout instead of requestAnimationFrame, because
140
+ // with requestAnimationFrame a flash of very high size
141
+ // can still occur although rare.
142
+ showTimeoutId = setTimeout( () => {
143
+ currentElement.style.visibility = previousVisibility;
144
+ }, 10 );
145
+ } );
146
+ } );
115
147
 
116
148
  // Watch for size changes
117
149
  let resizeObserver;
118
150
  if ( window.ResizeObserver && currentElement.parentElement ) {
119
151
  resizeObserver = new window.ResizeObserver( applyFitText );
120
152
  resizeObserver.observe( currentElement.parentElement );
153
+ resizeObserver.observe( currentElement );
121
154
  }
122
155
 
123
156
  // Cleanup function
124
157
  return () => {
158
+ // Cancel pending async operations
159
+ if ( hideFrameId !== null ) {
160
+ window.cancelAnimationFrame( hideFrameId );
161
+ }
162
+ if ( calculateFrameId !== null ) {
163
+ window.cancelAnimationFrame( calculateFrameId );
164
+ }
165
+ if ( showTimeoutId !== null ) {
166
+ clearTimeout( showTimeoutId );
167
+ }
168
+
125
169
  if ( resizeObserver ) {
126
170
  resizeObserver.disconnect();
127
171
  }
@@ -133,7 +177,14 @@ function useFitText( { fitText, name, clientId } ) {
133
177
  styleElement.remove();
134
178
  }
135
179
  };
136
- }, [ fitText, clientId, applyFitText, blockElement, hasFitTextSupport ] );
180
+ }, [
181
+ fitText,
182
+ clientId,
183
+ parentId,
184
+ applyFitText,
185
+ blockElement,
186
+ hasFitTextSupport,
187
+ ] );
137
188
 
138
189
  // Trigger fit text recalculation when content changes
139
190
  useEffect( () => {
@@ -1,84 +1,52 @@
1
1
  /**
2
2
  * Frontend fit text functionality.
3
3
  * Automatically detects and initializes fit text on blocks with the has-fit-text class.
4
+ * Supports both initial page load and Interactivity API client-side navigation.
4
5
  */
5
6
 
6
7
  /**
7
- * Internal dependencies
8
- */
9
- import { optimizeFitText } from './fit-text-utils';
10
-
11
- /**
12
- * Counter for generating unique element IDs.
13
- */
14
- let idCounter = 0;
15
-
16
- /**
17
- * Get or create a unique style element for a fit text element.
18
- *
19
- * @param {string} elementId Unique identifier for the element.
20
- * @return {HTMLElement} Style element.
21
- */
22
- function getOrCreateStyleElement( elementId ) {
23
- const styleId = `fit-text-${ elementId }`;
24
- let styleElement = document.getElementById( styleId );
25
- if ( ! styleElement ) {
26
- styleElement = document.createElement( 'style' );
27
- styleElement.id = styleId;
28
- document.head.appendChild( styleElement );
29
- }
30
- return styleElement;
31
- }
32
-
33
- /**
34
- * Generate a unique identifier for a fit text element.
35
- *
36
- * @param {HTMLElement} element The element to identify.
37
- * @return {string} Unique identifier.
8
+ * WordPress dependencies
38
9
  */
39
- function getElementIdentifier( element ) {
40
- if ( ! element.dataset.fitTextId ) {
41
- element.dataset.fitTextId = `fit-text-${ ++idCounter }`;
42
- }
43
- return element.dataset.fitTextId;
44
- }
10
+ import { store, getElement, getContext } from '@wordpress/interactivity';
45
11
 
46
12
  /**
47
- * Initialize fit text functionality for a single element.
48
- *
49
- * @param {HTMLElement} element Element with fit text enabled.
50
- */
51
- function initializeFitText( element ) {
52
- const elementId = getElementIdentifier( element );
53
-
54
- const applyFitText = () => {
55
- const styleElement = getOrCreateStyleElement( elementId );
56
- const elementSelector = `[data-fit-text-id=\"${ elementId }\"]`;
57
-
58
- // Style management callback
59
- const applyStylesFn = ( css ) => {
60
- styleElement.textContent = css;
61
- };
62
-
63
- optimizeFitText( element, elementSelector, applyStylesFn );
64
- };
65
-
66
- // Initial sizing
67
- applyFitText();
68
-
69
- // Watch for parent container resize
70
- if ( window.ResizeObserver && element.parentElement ) {
71
- const resizeObserver = new window.ResizeObserver( applyFitText );
72
- resizeObserver.observe( element.parentElement );
73
- }
74
- }
75
-
76
- /**
77
- * Initialize fit text on all elements with the has-fit-text class.
13
+ * Internal dependencies
78
14
  */
79
- function initializeAllFitText() {
80
- const elements = document.querySelectorAll( '.has-fit-text' );
81
- elements.forEach( initializeFitText );
82
- }
15
+ import { optimizeFitText } from './fit-text-utils';
83
16
 
84
- window.addEventListener( 'load', initializeAllFitText );
17
+ // Initialize via Interactivity API for client-side navigation
18
+ store( 'core/fit-text', {
19
+ callbacks: {
20
+ init() {
21
+ const context = getContext();
22
+ const { ref } = getElement();
23
+
24
+ const applyFontSize = ( fontSize ) => {
25
+ if ( fontSize === 0 ) {
26
+ ref.style.fontSize = '';
27
+ } else {
28
+ ref.style.fontSize = `${ fontSize }px`;
29
+ }
30
+ };
31
+
32
+ // Initial fit text optimization.
33
+ context.fontSize = optimizeFitText( ref, applyFontSize );
34
+
35
+ // Starts ResizeObserver to handle dynamic resizing.
36
+ if ( window.ResizeObserver && ref.parentElement ) {
37
+ const resizeObserver = new window.ResizeObserver( () => {
38
+ context.fontSize = optimizeFitText( ref, applyFontSize );
39
+ } );
40
+ resizeObserver.observe( ref.parentElement );
41
+ resizeObserver.observe( ref );
42
+
43
+ // Return cleanup function to be called when element is removed.
44
+ return () => {
45
+ if ( resizeObserver ) {
46
+ resizeObserver.disconnect();
47
+ }
48
+ };
49
+ }
50
+ },
51
+ },
52
+ } );
@@ -3,26 +3,14 @@
3
3
  * Uses callback-based approach for maximum code reuse between editor and frontend.
4
4
  */
5
5
 
6
- /**
7
- * Generate CSS rule for single text element.
8
- *
9
- * @param {string} elementSelector CSS selector for the text element
10
- * @param {number} fontSize Font size in pixels
11
- * @return {string} CSS rule string
12
- */
13
- function generateCSSRule( elementSelector, fontSize ) {
14
- return `${ elementSelector } { font-size: ${ fontSize }px !important; }`;
15
- }
16
-
17
6
  /**
18
7
  * Find optimal font size using simple binary search between 5-600px.
19
8
  *
20
- * @param {HTMLElement} textElement The text element
21
- * @param {string} elementSelector CSS selector for the text element
22
- * @param {Function} applyStylesFn Function to apply test styles
9
+ * @param {HTMLElement} textElement The text element
10
+ * @param {Function} applyFontSize Function that receives font size in pixels
23
11
  * @return {number} Optimal font size
24
12
  */
25
- function findOptimalFontSize( textElement, elementSelector, applyStylesFn ) {
13
+ function findOptimalFontSize( textElement, applyFontSize ) {
26
14
  const alreadyHasScrollableHeight =
27
15
  textElement.scrollHeight > textElement.clientHeight;
28
16
  let minSize = 5;
@@ -31,7 +19,7 @@ function findOptimalFontSize( textElement, elementSelector, applyStylesFn ) {
31
19
 
32
20
  while ( minSize <= maxSize ) {
33
21
  const midSize = Math.floor( ( minSize + maxSize ) / 2 );
34
- applyStylesFn( generateCSSRule( elementSelector, midSize ) );
22
+ applyFontSize( midSize );
35
23
 
36
24
  const fitsWidth = textElement.scrollWidth <= textElement.clientWidth;
37
25
  const fitsHeight =
@@ -51,25 +39,20 @@ function findOptimalFontSize( textElement, elementSelector, applyStylesFn ) {
51
39
 
52
40
  /**
53
41
  * Complete fit text optimization for a single text element.
54
- * Handles the full flow using callbacks for style management.
42
+ * Handles the full flow using callbacks for font size application.
55
43
  *
56
- * @param {HTMLElement} textElement The text element (paragraph, heading, etc.)
57
- * @param {string} elementSelector CSS selector for the text element
58
- * @param {Function} applyStylesFn Function to apply CSS styles (pass empty string to clear)
44
+ * @param {HTMLElement} textElement The text element (paragraph, heading, etc.)
45
+ * @param {Function} applyFontSize Function that receives font size in pixels (0 to clear, >0 to apply)
59
46
  */
60
- export function optimizeFitText( textElement, elementSelector, applyStylesFn ) {
47
+ export function optimizeFitText( textElement, applyFontSize ) {
61
48
  if ( ! textElement ) {
62
49
  return;
63
50
  }
64
51
 
65
- applyStylesFn( '' );
52
+ applyFontSize( 0 );
66
53
 
67
- const optimalSize = findOptimalFontSize(
68
- textElement,
69
- elementSelector,
70
- applyStylesFn
71
- );
54
+ const optimalSize = findOptimalFontSize( textElement, applyFontSize );
72
55
 
73
- const cssRule = generateCSSRule( elementSelector, optimalSize );
74
- applyStylesFn( cssRule );
56
+ applyFontSize( optimalSize );
57
+ return optimalSize;
75
58
  }
package/tsconfig.json CHANGED
@@ -20,6 +20,7 @@
20
20
  { "path": "../html-entities" },
21
21
  { "path": "../i18n" },
22
22
  { "path": "../icons" },
23
+ { "path": "../interactivity" },
23
24
  { "path": "../is-shallow-equal" },
24
25
  { "path": "../keycodes" },
25
26
  { "path": "../notices" },