Use ResizeObserver to recalculate the font size for amp-fit-text fixes #29556. #31317
Use ResizeObserver to recalculate the font size for amp-fit-text fixes #29556. #31317kristoferbaxter merged 50 commits intoampproject:masterfrom
Conversation
…verify that calculateFontSize_ is correctly called when a resize is observed
| }, | ||
| }); | ||
|
|
||
| if (window.ResizeObserver) { |
There was a problem hiding this comment.
no need to register in buildCallback if you already have it in layoutCallback
| this.unlisteners_ = []; | ||
|
|
||
| /** @private {ResizeObserver} */ | ||
| this.observer_ = null; |
There was a problem hiding this comment.
There is no need to store the observer, also unlistener doesn't need to be an array. I recommend
this.resizeObserverUnlistener_ = null;
Then in layoutCallback and unlayoutCallback you check the resizeObserverUnlistener_
|
|
||
| /** @override */ | ||
| layoutCallback() { | ||
| if (window.ResizeObserver) { |
| 'updateFontSize_' | ||
| ); | ||
| await ft.implementation_.layoutCallback(); | ||
| expect(setupUpdateFontSizeSpy).to.be.called; |
There was a problem hiding this comment.
It's unclear whether the updateFontSize_ is invoked by the resizeObserver or layoutCallback here. Better do
layoutCallback
// check called
change size
// check called
There was a problem hiding this comment.
I added the new tests, is this test more clear?
There was a problem hiding this comment.
expect(setupUpdateFontSizeSpy).to.be.called; once checks if the function has been called before. Use
calledOnce and calledTwice to be more specific
resizeObserver from buildCallback, modify unlistener pattern to store only a single unlistener for the resizeObserver. Modify tests to be more clear what the expected behavior is.
Merge parent changes into the child branch.
examples/amp-fit-text.amp.html
Outdated
| <!-- ## Introduction --><!-- | ||
| [`amp-fit-text`](/content/amp-dev/documentation/components/reference/amp-fit-text-v0.1.md) enables you to manage the size and fit of text within a given area. | ||
| --><!-- --> | ||
| <!doctype html> | ||
| <html ⚡> | ||
| <head> | ||
| <!-- ## Setup --> | ||
| <!-- | ||
| Include the `amp-fit-text` component. | ||
| --> |
There was a problem hiding this comment.
These <!-- ## --> blocks are customarily elaborated for amp.dev code samples, but this file will not be ported to amp.dev. I'd recommend either adding this example in that repo or removing annotations from this file.
examples/amp-fit-text.amp.html
Outdated
| <div class="square-medium"><amp-fit-text id="max medium" width="200" height="200" layout="responsive" max-font-size="18">Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt.</amp-fit-text></div> | ||
| <div class="square-large"><amp-fit-text id="max large" width="200" height="200" layout="responsive" max-font-size="18">Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt.</amp-fit-text></div> | ||
| </div> | ||
| </body></html> No newline at end of file |
|
|
||
| /** @override */ | ||
| layoutCallback() { | ||
| if (this.win.ResizeObserver) { |
There was a problem hiding this comment.
please flip to save a level of indentation:
if (!this.win.ResizeObserver) {
return;
}There was a problem hiding this comment.
Would it make more sense to just pull this part into the same if check as the following one? like
| if (this.win.ResizeObserver) { | |
| if (this.win.ResizeObserver && this.resizeObserverUnlistener_ === null) { |
There was a problem hiding this comment.
You still want to early return to save a level of indentation.
if (!this.win.ResizeObserver || this.resizeObserverUnlistener_) {
return;
}There was a problem hiding this comment.
In this case we want to call the final return statement whether or not this if block is true, this specifically relates to deciding whether we should attach a new observer, would it be better to extract the logic which attaches the listener to a helper method? so that the layout callback would look like this?
if (this.win.ResizeObserver && this.resizeObserverUnlistener_ === null) {
attachNewResizeObserver();
}
return this.mutateElement(() => {
this.updateFontSize_();
});
There was a problem hiding this comment.
Oh I see, apologies for missing that. In that case either way works :)
There was a problem hiding this comment.
Sounds good, Thanks!
| this.maxFontSize_ = -1; | ||
|
|
||
| /** @private {unlistenDef} */ | ||
| this.resizeObserverUnlistener_ = null; |
There was a problem hiding this comment.
Should this be initialized to () => {} so that we can never accidentally call null in the unlayoutCallback?
There was a problem hiding this comment.
it is also used as a signal to tell if resizeObserver has been initialized. I recommend initialize it to null but add the check to unlayoutCallback
There was a problem hiding this comment.
I set this to null to indicate that a resizeObserver has not been attached yet.
There was a problem hiding this comment.
Ah! I replied before I saw your comment, SGTM.
| it('renders', async () => { | ||
| const text = 'Lorem ipsum'; | ||
| return getFitText(text).then((ft) => { | ||
| return getFitText(text).then(async (ft) => { |
There was a problem hiding this comment.
If this is moving to an async method, you can use await syntax instead of relying on the thenable directly.
use await, and checking whether this.resizeObserverUnlistener_ is null before attempting to call it.
Use to.not.be.null notation instead of to.not.equal(null) Co-authored-by: Caroline Liu <10456171+caroqliu@users.noreply.github.com>
| /** @private {number} */ | ||
| this.maxFontSize_ = -1; | ||
|
|
||
| /** @private {unlistenDef} */ |
| /** @override */ | ||
| unlayoutCallback() { | ||
| if (this.resizeObserverUnlistener_ !== null) { | ||
| this.resizeObserverUnlistener_(); |
There was a problem hiding this comment.
also set this.resizeObserverUnlistener_ back to null.
| </style> | ||
| </head> | ||
| <body> | ||
| <!-- |
There was a problem hiding this comment.
Are these HTML comments necessary in the doc?
There was a problem hiding this comment.
I added them just for clarity and readability for anyone who would be looking at this file, but I can pull them out if they aren't necessary.
| /** @private {number} */ | ||
| this.maxFontSize_ = -1; | ||
|
|
||
| /** @private {?unlistenDef} */ |
There was a problem hiding this comment.
Should this be function? Where is unlistenDef defined?
There was a problem hiding this comment.
UnlistenDef is defined in amp.extern.js (https://github.com/ampproject/amphtml/blob/master/build-system/externs/amp.extern.js#L414-L418) and refers to a function which specifically removes a listener, in this case the resizeObserver. I think its just a shade more precise than the function type declaration for this usage.
There was a problem hiding this comment.
Oh I see - then should this be capitalized?
| /** @private {?unlistenDef} */ | |
| /** @private {?UnlistenDef} */ |
| const content = ft.querySelector('.i-amphtml-fit-text-content'); | ||
| expect(content).to.not.be.null; | ||
| expect(ft.textContent).to.equal(text); | ||
| await ft.implementation_.unlayoutCallback(); |
There was a problem hiding this comment.
FMI: Why is this line added to test that the component renders?
There was a problem hiding this comment.
I initially added this because without this call the ResizeObserver unlistener, was not being correctly triggered, however after debugging this appears to have been related to a different issue so I have removed this line in this test.
| await new Promise((resolve) => { | ||
| setTimeout(() => { | ||
| resolve(); | ||
| }, 90); |
There was a problem hiding this comment.
Why 90? Could be helpful to add an inline comment on why this value is meaningful.
There was a problem hiding this comment.
In this case 90 was arbitrarily chosen, i'll make it 100 and then add a comment to indicate why it was chosen.
There was a problem hiding this comment.
I wasn't sure if it was intentionally related to the throttle rate. Thanks for adding a comment :)
There was a problem hiding this comment.
The idea is to wait for the ResizeObserver to fire, but not until the next function call due to throttle. So any value less than 100 should be good.
There was a problem hiding this comment.
It turns out 100ms exactly is a border case which can pass but its not clear whats happening so I am gonna re-set it to 90 and adjust my comments accordingly.
There was a problem hiding this comment.
Oh got it, thanks both for clarifying!
remove manual unlayoutCallback from test.
Add ResizeObserver to
amp-fit-textwhich triggersupdateFontSize_when the component children:measurer_orcontent_changes size.Add unit test to verify that
updateFontSize_is appropriately called when the component size changes.This change fixes #29556.
In addition to fixing this accessibility issue this change adds a new feature. The resizeObserver will now recalculate the font size if the content of the
amp-fit-textcomponent changes.