Skip to content

Use ResizeObserver to recalculate the font size for amp-fit-text fixes #29556. #31317

Merged
kristoferbaxter merged 50 commits intoampproject:masterfrom
rbeckthomas:fit-text-ally
Jan 12, 2021
Merged

Use ResizeObserver to recalculate the font size for amp-fit-text fixes #29556. #31317
kristoferbaxter merged 50 commits intoampproject:masterfrom
rbeckthomas:fit-text-ally

Conversation

@rbeckthomas
Copy link
Copy Markdown
Contributor

@rbeckthomas rbeckthomas commented Nov 24, 2020

Add ResizeObserver to amp-fit-text which triggers updateFontSize_ when the component children: measurer_ or content_ 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-text component changes.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Nov 24, 2020

CLA assistant check
All committers have signed the CLA.

@rbeckthomas rbeckthomas marked this pull request as ready for review November 25, 2020 00:19
@zhouyx zhouyx requested review from caroqliu and zhouyx November 25, 2020 03:23
},
});

if (window.ResizeObserver) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to register in buildCallback if you already have it in layoutCallback

this.unlisteners_ = [];

/** @private {ResizeObserver} */
this.observer_ = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use this.win instead.

'updateFontSize_'
);
await ft.implementation_.layoutCallback();
expect(setupUpdateFontSizeSpy).to.be.called;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear whether the updateFontSize_ is invoked by the resizeObserver or layoutCallback here. Better do

layoutCallback
// check called
change size
// check called

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the new tests, is this test more clear?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Comment on lines +1 to +10
<!-- ## 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.
-->
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

<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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: new line eof


/** @override */
layoutCallback() {
if (this.win.ResizeObserver) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please flip to save a level of indentation:

if (!this.win.ResizeObserver) {
  return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to just pull this part into the same if check as the following one? like

Suggested change
if (this.win.ResizeObserver) {
if (this.win.ResizeObserver && this.resizeObserverUnlistener_ === null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still want to early return to save a level of indentation.

if (!this.win.ResizeObserver || this.resizeObserverUnlistener_) {
  return;
}

Copy link
Copy Markdown
Contributor Author

@rbeckthomas rbeckthomas Dec 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, apologies for missing that. In that case either way works :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, Thanks!

this.maxFontSize_ = -1;

/** @private {unlistenDef} */
this.resizeObserverUnlistener_ = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be initialized to () => {} so that we can never accidentally call null in the unlayoutCallback?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set this to null to indicate that a resizeObserver has not been attached yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is moving to an async method, you can use await syntax instead of relying on the thenable directly.

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} */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: `{?unlistenDef}

/** @override */
unlayoutCallback() {
if (this.resizeObserverUnlistener_ !== null) {
this.resizeObserverUnlistener_();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also set this.resizeObserverUnlistener_ back to null.

</style>
</head>
<body>
<!--
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these HTML comments necessary in the doc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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} */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be function? Where is unlistenDef defined?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see - then should this be capitalized?

Suggested change
/** @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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FMI: Why is this line added to test that the component renders?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 90? Could be helpful to add an inline comment on why this value is meaningful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case 90 was arbitrarily chosen, i'll make it 100 and then add a comment to indicate why it was chosen.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if it was intentionally related to the throttle rate. Thanks for adding a comment :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh got it, thanks both for clarifying!

@rbeckthomas rbeckthomas changed the title Use ResizeObserver to recalculate the font size for amp-fit-text fixes 29556. Use ResizeObserver to recalculate the font size for amp-fit-text fixes #29556. Jan 5, 2021
@rbeckthomas rbeckthomas requested a review from caroqliu January 6, 2021 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

amp-fit-text: Content is unreadable when text style is adjusted

6 participants