CARVIEW |
Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Add: Fit text/stretch text block supports and variation. #71904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
6dc2100
to
c635583
Compare
Size Change: +1.58 kB (+0.08%) Total Size: 1.96 MB
ℹ️ View Unchanged
|
c635583
to
28b0215
Compare
@@ -0,0 +1,3 @@ | |||
.has-fit-text { | |||
white-space: nowrap !important; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fit or Stretched Text can not wrap otherwise it does not make sense. But one can have multiple paragraphs on the same container.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're already adding this rule in block-library. Do we need the redundancy here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the redundancy 👍
let maxSize = 200; | ||
let bestSize = minSize; | ||
|
||
while ( minSize <= maxSize ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, we would avoid this loop and use a direct mathematical calculation. I tried to find a solution but was unable to find one that was reliable in different containers (with or without other fitted children) and that worked across different font families and sizes. Some third-party libraries that use a mathematical calculation rely on the user providing a "compressor" value to optimize how much the text scales, for example: https://github.com/adactio/FitText.js/blob/master/fittext.js
.
In the picture, we can see a comparison of our implementation with a third-party implementation using default settings.
What our implementation does is find the largest possible font size that fits in a container without causing scrolling or hiding the text. There doesn't seem to be a direct method that works across various containers and fonts without user tuning. I think, ideally, we should avoid requiring user tuning, even if our code is a little more complex.
Having the loop should not cause a significant performance impact, as we optimize the search using a binary search. In the worst-case scenario, this loop runs only 7 times.
The logic for findOptimalFontSize is contained in a single function, so users don't need to worry about how it works, and if we agree on this approach I will make sure the function is tested with automatic tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we at least initially try and calculate the size with an offscreen clone of the element to avoid all the layout changes in the visible element? Is this possible or there are restrictions about the parent containers?
A lot of the changes on this PR are the updates of fixture on our unit tests (because we are adding a new attribute to the paragraph). The last commit here was automatically generated and just contains the fixture updates. |
cde224a
to
5e1563e
Compare
@jorgefilipecosta I just gave this a quick test run and I don't believe it's working as expected: ![]() The text should not overflow the container width, correct? |
a6b8d75
to
7d122b2
Compare
Hi @tellthemachines, That issue was occurring because we were expanding elements relative to the parent's available width, while the post content itself is full width. We have a CSS rule that constrains non-full-width blocks to a smaller max-width, but this rule wasn't being applied correctly on our previous logic. The issue has now been resolved. Note that it's still possible to achieve full-width expansion by for example inserting a grid block, setting it to full width, and placing the stretch paragraph inside that grid container. ![]() ![]()
|
I tested this, but it doesn't seem to be working on the frontend. Also, is there a minimum size that will fit? I'm finding that very short text isn't stretching. |
Hi @scruffian, we are adding a new module for the front end, so for example if we switch to this branch while npm run dev is watching things will not work.
There is no minimum size that will fit, but we were stretching until a max font size of 200 which may made it look like very small text like one char was not stretching. I changed this value and now we stretch until a max of 600, we can change this to an even bigger value, but I think 600 should provide good results. |
I can confirm that this is working well now! Should we also add a variation for stretchy headings? |
// Get content from common content attributes | ||
return blockAttributes?.content || blockAttributes?.value || ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we look at the first content: 'role'
attribute in the schema rather than hardcoded names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or perhaps the block supports config could allow optionally specify the attribute key.
The button block might be an example where the attribute isn't the first content role and the attribute is called 'text'. The rich-text
type might also be an interesting way to pick the attribute, but then that rules out plain stretchy text.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. The supports config thing makes sense. Still, could keep role
as a fallback, which is IMO better than .content/.value
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve been thinking about this, and I believe we should recompute on any attribute change. Even non-content attributes like padding or margin alter the available space, and a custom block might toggle a boolean that adds a thick border with spacing. In short, any attribute change can affect sizing so recomputing on every change seems the safest and simplest approach, on #71904 we were reconputing on any attribute change of the descendent of the group block (getBlocks() changed), here I tried to optimize but I don't think it was a good idea.
* @param {HTMLElement} textElement The text element (paragraph, heading, etc.) | ||
* @param {string} elementSelector CSS selector for the text element | ||
* @param {Function} applyStylesFn Function to apply CSS styles (pass empty string to clear) | ||
* @return {Object} Object with cssRule, originalSize, and optimalSize |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the callers of optimizeFitText
use the return value. Discard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it was discarded 👍
// Get original font size (clears styles internally) | ||
const originalSize = getOriginalFontSize( textElement, applyStylesFn ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we inline this function, we'll improve a few things:
- it becomes evident that
getOriginalFontSize
's codepath for a falsytextElement
is never run - the side effect
applyStylesFn( '' )
is made more prominent (the function name starting withget
is a bit misleading)
Also, there a lot of superfluous comments in this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes total sense this logic was simplified we don't even need to retrieve explicitly the original font size.
const originalSize = getOriginalFontSize( textElement, applyStylesFn ); | ||
|
||
// Find optimal font size using binary search | ||
const optimalSize = findOptimalFontSize( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand wanting to encapsulate this in a function, but don't export it unless needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea was to export so it can be imported on the tests file. But until tests we don't need the export so it was removed.
} | ||
|
||
/** | ||
* Find optimal font size using simple binary search between 5-200px. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Range (–200) doesn't correspond to actual constants (600).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was 200 before I updated the comment to match the new value.
@@ -0,0 +1,3 @@ | |||
.has-fit-text { | |||
white-space: nowrap !important; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're already adding this rule in block-library. Do we need the redundancy here?
return ( | ||
<InspectorControls group="typography"> | ||
<ToolsPanelItem | ||
hasValue={ () => !! fitText } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this should already be a boolean, right? Why the cast? Same question for checked={ !! fitText }
below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed 👍
// Here Math.random is ok to generate ids they don't need to cryptographically secure. | ||
// eslint-disable-next-line no-restricted-syntax | ||
'fit-text-' + Math.random().toString( 36 ).substring( 2, 11 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just a counter living in the module? Also, typo in the comment.
(I know the odds are next to nil, but playing dice with Math.random()
and not checking for collisions makes me uncomfortable 😆)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right a counter works well here, it was simplified.
// Store observer for cleanup if needed | ||
element._fitTextObserver = resizeObserver; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed.
I suppose that if you wanted to be extra careful you could .disconnect
the observer if element
no longer exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
lib/block-supports/typography.php
Outdated
* @return string Filtered block content. | ||
*/ | ||
function gutenberg_render_typography_support( $block_content, $block ) { | ||
if ( isset( $block['attrs']['fitText'] ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to check if true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to empty which checks for false.
9083d58
to
bfb2297
Compare
Hi @mcsf, thank you a lot for your review, I think I addressed all your comments. |
8c1f5f6
to
f4c18c5
Compare
When I toggle the "Fit Text" option I get this warning in my console:
|
This commit backports to core the php changes required for the fitText implementation which was worked on at WordPress/gutenberg#71904. It can be merged even before the packages are updated because unless the fitText support is used nothing happens.
Flaky tests detected in 94a6927. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/18346332826
|
Hi @ntsekouras, I applied your suggestion and removed fixture changes. Also fixed end to end tests and created a backport PR at WordPress/wordpress-develop#10190. CI is green I think this is ready to merge. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks for all your work here Jorge!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! When we merge this we'll have new dependencies - do we need to remind everyone to rebuild their local projects?
As a followup, it might be useful to be able to transform between a stretch heading and a stretchy paragraph.
I don't think we need an explicit reminder the only issue will be that without a rebuild the fit text may not appear correct on the frontend. But with time people will probably restart ( doing npm run dev again) and endup rebuilding anyway so I think it is fine. If someone notices an issue with fit text on frontend (because of not passing by a rebuild) may endup on this PR and see a rebuild is necessary.
Agreed, I will have a look into this. Thank you all for the reviews! |
Adds fitText block support definition to enable proper validation and autocomplete for the fitText feature added in WordPress#71904. Fixes WordPress#72190
* Add fitText field to block.json schema Adds fitText block support definition to enable proper validation and autocomplete for the fitText feature added in WordPress#71904. Fixes WordPress#72190 * Move fitText to supports.typography.fitText location Addresses review feedback to place fitText under typography supports rather than as a top-level support property. Co-authored-by: aditya241104 <adityashah2411@git.wordpress.org> Co-authored-by: t-hamano <wildworks@git.wordpress.org>
…71904) Co-authored-by: jorgefilipecosta <jorgefilipecosta@git.wordpress.org> Co-authored-by: mcsf <mcsf@git.wordpress.org> Co-authored-by: talldan <talldanwp@git.wordpress.org> Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org> Co-authored-by: scruffian <scruffian@git.wordpress.org> Co-authored-by: tellthemachines <isabel_brison@git.wordpress.org>
* Add fitText field to block.json schema Adds fitText block support definition to enable proper validation and autocomplete for the fitText feature added in WordPress#71904. Fixes WordPress#72190 * Move fitText to supports.typography.fitText location Addresses review feedback to place fitText under typography supports rather than as a top-level support property. Co-authored-by: aditya241104 <adityashah2411@git.wordpress.org> Co-authored-by: t-hamano <wildworks@git.wordpress.org>
This PR introduces a new fitText block support that enables automatic font size adjustment to fit text within its container boundaries. Unlike previous approaches that created new blocks or variations, this implementation adds reusable block support that can be adopted by any text-based block.
Alternative to #71812 and #71017 - Instead of creating a variation for the group block or a new standalone block, this adds a new block support named
fitText
(following common naming accorss other libs) that any text block can implement. By default, paragraph and heading core blocks now support this functionality.Technical Implementation
Architecture
fit-text-utils.js
Key Components
packages/block-editor/src/hooks/fit-text.js
- Editor hook and controlspackages/block-editor/src/utils/fit-text-frontend.js
- Frontend script modulepackages/block-editor/src/utils/fit-text-utils.js
- Shared editor and front logiclib/block-supports/typography.php
- Server-side script enqueueingUsage
For Users
For Developers
Enable fitText support in any text block:
Block Variation
A "Stretchy Paragraph" variation is available in the inserter for quick access to fit text functionality.
Files Changed
Core Changes
lib/block-supports/typography.php
- Frontend script enqueueingpackages/block-editor/src/hooks/fit-text.js
- Main editor implementation (269 lines)packages/block-editor/src/utils/fit-text-utils.js
- Shared utilities (116 lines)packages/block-editor/src/utils/fit-text-frontend.js
- Frontend module (94 lines)Block Updates
packages/block-library/src/paragraph/block.json
- Added fitText supportpackages/block-library/src/heading/block.json
- Added fitText supportpackages/block-library/src/paragraph/variations.js
- New "Stretchy Paragraph" variationStyling & Integration
packages/block-editor/src/hooks/fit-text.scss
- Editor stylespackages/block-library/src/common.scss
- Frontend stylesPerformance Considerations
Testing
Tested Scenarios
Video
Screen.Recording.2025-09-25.at.21.57.30.mov