Skip to content

Commit 7040958

Browse files
jorgefilipecostaluisherranz
authored andcommitted
Fix: Make fit text work with the interativity API. (#72923)
Co-authored-by: jorgefilipecosta <[email protected]> Co-authored-by: luisherranz <[email protected]>
1 parent 97a89e0 commit 7040958

File tree

9 files changed

+113
-155
lines changed

9 files changed

+113
-155
lines changed

backport-changelog/6.9/10455.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
https://github.com/WordPress/wordpress-develop/pull/10455
2+
3+
* https://github.com/WordPress/gutenberg/pull/72923

lib/block-supports/typography.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,20 @@ function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_
246246
function gutenberg_render_typography_support( $block_content, $block ) {
247247
if ( ! empty( $block['attrs']['fitText'] ) && ! is_admin() ) {
248248
wp_enqueue_script_module( '@wordpress/block-editor/utils/fit-text-frontend' );
249+
250+
// Add Interactivity API directives for fit text to work with client-side navigation.
251+
if ( ! empty( $block_content ) ) {
252+
$processor = new WP_HTML_Tag_Processor( $block_content );
253+
if ( $processor->next_tag() ) {
254+
if ( ! $processor->get_attribute( 'data-wp-interactive' ) ) {
255+
$processor->set_attribute( 'data-wp-interactive', true );
256+
}
257+
$processor->set_attribute( 'data-wp-context---core-fit-text', 'core/fit-text::{"fontSize":""}' );
258+
$processor->set_attribute( 'data-wp-init---core-fit-text', 'core/fit-text::callbacks.init' );
259+
$processor->set_attribute( 'data-wp-style--font-size', 'core/fit-text::context.fontSize' );
260+
$block_content = $processor->get_updated_html();
261+
}
262+
}
249263
}
250264

251265
if ( ! isset( $block['attrs']['style']['typography']['fontSize'] ) ) {

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/block-editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@wordpress/html-entities": "file:../html-entities",
7676
"@wordpress/i18n": "file:../i18n",
7777
"@wordpress/icons": "file:../icons",
78+
"@wordpress/interactivity": "file:../interactivity",
7879
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
7980
"@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts",
8081
"@wordpress/keycodes": "file:../keycodes",

packages/block-editor/src/hooks/fit-text.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,15 @@ function useFitText( { fitText, name, clientId } ) {
9090

9191
const blockSelector = `#block-${ clientId }`;
9292

93-
const applyStylesFn = ( css ) => {
94-
styleElement.textContent = css;
93+
const applyFontSize = ( fontSize ) => {
94+
if ( fontSize === 0 ) {
95+
styleElement.textContent = '';
96+
} else {
97+
styleElement.textContent = `${ blockSelector } { font-size: ${ fontSize }px !important; }`;
98+
}
9599
};
96100

97-
optimizeFitText( blockElement, blockSelector, applyStylesFn );
101+
optimizeFitText( blockElement, applyFontSize );
98102
}, [ blockElement, clientId, hasFitTextSupport, fitText ] );
99103

100104
useEffect( () => {
Lines changed: 40 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,51 @@
11
/**
22
* Frontend fit text functionality.
33
* 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.
45
*/
56

67
/**
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
389
*/
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';
4511

4612
/**
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
7814
*/
79-
function initializeAllFitText() {
80-
const elements = document.querySelectorAll( '.has-fit-text' );
81-
elements.forEach( initializeFitText );
82-
}
15+
import { optimizeFitText } from './fit-text-utils';
8316

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+
42+
// Return cleanup function to be called when element is removed.
43+
return () => {
44+
if ( resizeObserver ) {
45+
resizeObserver.disconnect();
46+
}
47+
};
48+
}
49+
},
50+
},
51+
} );

packages/block-editor/src/utils/fit-text-utils.js

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,14 @@
33
* Uses callback-based approach for maximum code reuse between editor and frontend.
44
*/
55

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-
176
/**
187
* Find optimal font size using simple binary search between 5-600px.
198
*
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
2311
* @return {number} Optimal font size
2412
*/
25-
function findOptimalFontSize( textElement, elementSelector, applyStylesFn ) {
13+
function findOptimalFontSize( textElement, applyFontSize ) {
2614
const alreadyHasScrollableHeight =
2715
textElement.scrollHeight > textElement.clientHeight;
2816
let minSize = 5;
@@ -31,7 +19,7 @@ function findOptimalFontSize( textElement, elementSelector, applyStylesFn ) {
3119

3220
while ( minSize <= maxSize ) {
3321
const midSize = Math.floor( ( minSize + maxSize ) / 2 );
34-
applyStylesFn( generateCSSRule( elementSelector, midSize ) );
22+
applyFontSize( midSize );
3523

3624
const fitsWidth = textElement.scrollWidth <= textElement.clientWidth;
3725
const fitsHeight =
@@ -51,25 +39,20 @@ function findOptimalFontSize( textElement, elementSelector, applyStylesFn ) {
5139

5240
/**
5341
* 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.
5543
*
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)
5946
*/
60-
export function optimizeFitText( textElement, elementSelector, applyStylesFn ) {
47+
export function optimizeFitText( textElement, applyFontSize ) {
6148
if ( ! textElement ) {
6249
return;
6350
}
6451

65-
applyStylesFn( '' );
52+
applyFontSize( 0 );
6653

67-
const optimalSize = findOptimalFontSize(
68-
textElement,
69-
elementSelector,
70-
applyStylesFn
71-
);
54+
const optimalSize = findOptimalFontSize( textElement, applyFontSize );
7255

73-
const cssRule = generateCSSRule( elementSelector, optimalSize );
74-
applyStylesFn( cssRule );
56+
applyFontSize( optimalSize );
57+
return optimalSize;
7558
}

packages/block-editor/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
{ "path": "../html-entities" },
2121
{ "path": "../i18n" },
2222
{ "path": "../icons" },
23+
{ "path": "../interactivity" },
2324
{ "path": "../is-shallow-equal" },
2425
{ "path": "../keycodes" },
2526
{ "path": "../notices" },

test/e2e/specs/editor/blocks/fit-text.spec.js

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -379,28 +379,18 @@ test.describe( 'Fit Text', () => {
379379
await expect( heading ).toBeVisible();
380380
await expect( heading ).toHaveClass( /has-fit-text/ );
381381

382-
// Verify data attribute is set (added by frontend script)
383-
const fitTextId = await heading.getAttribute( 'data-fit-text-id' );
384-
expect( fitTextId ).toBeTruthy();
385-
386-
// Verify style element exists for this fit text instance.
387-
const styleElement = page.locator(
388-
`style#fit-text-${ fitTextId }`
389-
);
390-
await expect( styleElement ).toBeAttached();
382+
const inlineStyle = await heading.getAttribute( 'style' );
383+
expect( inlineStyle ).toContain( 'font-size' );
384+
expect( inlineStyle ).toMatch( /font-size:\s*\d+px/ );
391385

392386
const computedFontSize = await heading.evaluate( ( el ) => {
393387
return window.getComputedStyle( el ).fontSize;
394388
} );
395389

396-
const styleContent = await styleElement.textContent();
397-
const fontSizeMatch = styleContent.match(
398-
/font-size:\s*(\d+(?:\.\d+)?)px/
399-
);
400-
expect( fontSizeMatch ).toBeTruthy();
401-
const expectedFontSize = parseFloat( fontSizeMatch[ 1 ] );
402-
403-
expect( parseFloat( computedFontSize ) ).toBe( expectedFontSize );
390+
// Verify font size is actually applied and is a reasonable value
391+
const fontSize = parseFloat( computedFontSize );
392+
expect( fontSize ).toBeGreaterThan( 0 );
393+
expect( fontSize ).toBeLessThan( 600 );
404394
} );
405395

406396
test( 'should resize text on window resize on the frontend', async ( {
@@ -426,48 +416,41 @@ test.describe( 'Fit Text', () => {
426416

427417
const heading = page.locator( 'h2.has-fit-text' );
428418

429-
// Wait for fit text to initialize (verify frontend script ran)
430-
await heading.waitFor( { state: 'attached' } );
431-
const fitTextId = await heading.getAttribute( 'data-fit-text-id' );
432-
expect( fitTextId ).toBeTruthy();
419+
// Wait for fit text to initialize
420+
await heading.waitFor( { state: 'visible' } );
421+
await expect( heading ).toHaveClass( /has-fit-text/ );
433422

434-
// Verify style element exists for this fit text instance
435-
const styleElement = page.locator(
436-
`style#fit-text-${ fitTextId }`
423+
// Wait for inline style to be applied
424+
await page.waitForFunction(
425+
() => {
426+
const el = document.querySelector( 'h2.has-fit-text' );
427+
return el && el.style.fontSize && el.style.fontSize !== '';
428+
},
429+
{ timeout: 5000 }
437430
);
438-
await styleElement.waitFor( { state: 'attached' } );
439431

440432
const initialFontSize = await heading.evaluate( ( el ) => {
441433
return window.getComputedStyle( el ).fontSize;
442434
} );
443435

444-
// Capture style content before resize
445-
const styleBeforeResize = await styleElement.textContent();
436+
const initialInlineStyle = await heading.getAttribute( 'style' );
446437

447438
await page.setViewportSize( { width: 440, height: 720 } );
448439

449-
// Wait for fit text to recalculate (style content changes)
440+
// Wait for inline font-size style to change after resize
450441
await page.waitForFunction(
451-
( { styleId, previousContent } ) => {
452-
const style = document.getElementById( styleId );
442+
( previousStyle ) => {
443+
const el = document.querySelector( 'h2.has-fit-text' );
453444
return (
454-
style &&
455-
style.textContent !== previousContent &&
456-
style.textContent.trim().length > 0
445+
el &&
446+
el.style.fontSize &&
447+
el.getAttribute( 'style' ) !== previousStyle
457448
);
458449
},
459-
{
460-
styleId: `fit-text-${ fitTextId }`,
461-
previousContent: styleBeforeResize,
462-
},
450+
initialInlineStyle,
463451
{ timeout: 5000 }
464452
);
465453

466-
// Verify the same element instance is maintained (ID unchanged)
467-
const fitTextIdAfterResize =
468-
await heading.getAttribute( 'data-fit-text-id' );
469-
expect( fitTextIdAfterResize ).toBe( fitTextId );
470-
471454
const newFontSize = await heading.evaluate( ( el ) => {
472455
return window.getComputedStyle( el ).fontSize;
473456
} );
@@ -509,17 +492,18 @@ test.describe( 'Fit Text', () => {
509492

510493
const fitTextParagraph = page.locator( 'p.has-fit-text' );
511494

512-
// Wait for fit text to initialize (verify frontend script ran)
495+
// Wait for fit text to initialize
513496
await fitTextParagraph.waitFor( { state: 'visible' } );
514-
const fitTextId =
515-
await fitTextParagraph.getAttribute( 'data-fit-text-id' );
516-
expect( fitTextId ).toBeTruthy();
497+
await expect( fitTextParagraph ).toHaveClass( /has-fit-text/ );
517498

518-
// Verify style element exists for this fit text instance
519-
const styleElement = page.locator(
520-
`style#fit-text-${ fitTextId }`
499+
// Wait for inline style to be applied
500+
await page.waitForFunction(
501+
() => {
502+
const el = document.querySelector( 'p.has-fit-text' );
503+
return el && el.style.fontSize && el.style.fontSize !== '';
504+
},
505+
{ timeout: 5000 }
521506
);
522-
await expect( styleElement ).toBeAttached();
523507

524508
const paragraphs = page.locator( 'p' );
525509

0 commit comments

Comments
 (0)