Skip to content
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

fix(material/tabs): ensure the ink bar realigns when the tab header changes dimensions #24885

Merged
merged 1 commit into from
May 18, 2022

Conversation

hilts-vaughan
Copy link
Contributor

@hilts-vaughan hilts-vaughan commented May 5, 2022

This should fix #6130 and also #4648?. It uses ResizeObserver to ensure we get notifications about the dimensions of the tabs when they change. The layout change will fire synchronously.

It may also fix #3048.

const itemsTracked = new Set<MatPaginatedTabHeaderItem>();
const itemContainerItemChanged = new Subject<void>();

const observer = new ResizeObserver(() => {
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be feature detected since it could run in a server environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does Angular have their own mechanism for this or can I just do a window.ResizeObserver check?

Copy link
Member

Choose a reason for hiding this comment

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

You can get it from this._platform.isBrowser, although I think I'd prefer to use typeof ResizeObserver === 'function' since it would avoid errors on browsers that don't support ResizeObserver.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a check, thanks.

src/material/tabs/paginated-tab-header.ts Outdated Show resolved Hide resolved
const itemsTracked = new Set<MatPaginatedTabHeaderItem>();
const itemContainerItemChanged = new Subject<void>();

const observer = new ResizeObserver(() => {
Copy link
Member

Choose a reason for hiding this comment

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

You can get it from this._platform.isBrowser, although I think I'd prefer to use typeof ResizeObserver === 'function' since it would avoid errors on browsers that don't support ResizeObserver.

src/material/tabs/paginated-tab-header.ts Outdated Show resolved Hide resolved
src/material/tabs/paginated-tab-header.ts Outdated Show resolved Hide resolved
src/material/tabs/tab-header.spec.ts Outdated Show resolved Hide resolved
@hilts-vaughan
Copy link
Contributor Author

I've cleaned it up a bit. If you have suggestions for the test, I would love to hear them. We can also do some other kind of test or just not a write at all if you're worried about it introducing flakiness if the component is expected to not be trouble in the future.

Copy link
Member

@crisbeto crisbeto left a comment

Choose a reason for hiding this comment

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

It seems like the CI didn't run for some reason 😕

startWith(this._items),
map(() => {
for (const item of this._items.toArray()) {
this._ngZone.run(() => {
Copy link
Member

Choose a reason for hiding this comment

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

This still hasn't been changed to runOutSizeAngular.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I forgot to push.

? new ResizeObserver(() => {
itemContainerItemChanged.next();
})
: {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that we should be stubbing out an observable. You can use the EMPTY observable from rxjs instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're not. We're stubbing out the RO function. I could also return null and check for null down below. I don't think EMPTY works.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I see. I find this side-effect-ey use of map confusing. Here's how I would've implemented it:

  1. Keep the merge(dirChange, resize, this._items.changes) above as is.
  2. Have a utility that looks like this:
private _itemsResized(): Observable<void> {
  if (typeof ResizeObserver !== 'function') {
    return EMPTY;
  }

  return this._items.changes.pipe(
    startWith(this._items),
    switchMap(
      (items: QueryList<MatPaginatedTabHeaderItem>) =>
        new Observable((observer: Observer<void>) =>
          this._ngZone.runOutsideAngular(() => {
            const resizeObserver = new ResizeObserver(() => observer.next());
            items.forEach(item => resizeObserver.observe(item.elementRef.nativeElement));
            return () => resizeObserver.disconnect();
          }),
        ),
    ),
    // Skip the first emit since the resize observer emits when an item is observed.
    skip(1),
  );
}
  1. Change the merge call above to merge(dirChange, resize, this._items.changes, this._itemsResized()).

This approach fixes the following issues:

  1. You generally don't want side effects from your map operator.
  2. The ResizeObserver wasn't being disconnected when the subscription is dropped.
  3. It avoids the extra call when the elements are observed for the first time.
  4. The ResizeObserver itself wasn't being created outside the NgZone.

Note that this is slightly more wasteful when it comes to recreating resize observers, but I think that it's not a big deal in this case since tabs don't tend to change very often. If it becomes a problem, we can start keeping track of which items are being observed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I adopted a similar implementation. I did add an "startWith" at the end to fire a single emission which is what the old code was doing. This is still important for the initial layout IIRC.

Copy link
Member

Choose a reason for hiding this comment

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

You can reintroduce the first render by removing the skip(1). I added it, because the first emit seems redundant to me. It fires when the elements are observed for the first time, but the initial render should already be correct since we do it right after the elements are inserted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removing skip will fire it on the RO layout which causes the rest to fail because as it turns out, the fakeAsyncZone doesn't actually capture the ResizeObserver tick (even though it technically runs before the paint, it's not before the call stack is cleared). This explains why that test passed and was not flaky.

We could remove the test and not bother or perhaps try to a stability check hack to fix that if you don't think the first synchronous emission is needed.

Copy link
Member

Choose a reason for hiding this comment

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

Okay, that explains why the test was passing. I'd rather avoid the test than have it fire on init, because it has the potential to cause layout thrashing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I removed the test for now to keep it simple especially since MDC is coming "soon". I talked with some internal folks and these links should be OK to share:

Before: https://screencast.googleplex.com/cast/NTA3MjkzNjU0MjE0MjQ2NHw0NTU1YzBmNi1lOA
After: https://screencast.googleplex.com/cast/NjI3MjI0NjU2Njg3OTIzMnw0MjBiMDcxZS0zNQ

This shows the stuff working in lieu of some tests.

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we get rid of the startWith(undefined) as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should be gone. I hadn't realize that pushing would immediately send it for re-review, unlike some other tools. :)

@hilts-vaughan hilts-vaughan force-pushed the fix-tab-resizing branch 2 times, most recently from 3dfef13 to 83478a0 Compare May 17, 2022 19:25
@hilts-vaughan hilts-vaughan force-pushed the fix-tab-resizing branch 2 times, most recently from 5bf036c to 91bb1ef Compare May 17, 2022 20:40
@crisbeto
Copy link
Member

So the change LGTM, but for some reason the CI didn't run. Can you try cherry-picking the commit to a new branch and opening another PR?

@hilts-vaughan
Copy link
Contributor Author

The CI system says it's missing some labels. Do I need to add some labels on the new PR? Or this one perhaps?

@crisbeto
Copy link
Member

The label checks are separate. Usually it queues up another 11 jobs that don't show up here for some reason. This has happened for other people and CircleCI haven't been able to figure out why. There was a theory that it might be because you aren't watching the fork that you're opening the PR from.

@hilts-vaughan
Copy link
Contributor Author

Opened #24932. I'll adjust the description etc if the CI runs.

@hilts-vaughan
Copy link
Contributor Author

(It also tells me that the CI workflow is pending approval and links me to https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks -- claiming a maintainer has to approve my CI run on this PR)

@crisbeto
Copy link
Member

Did you try clicking the "Watch" button on your fork? Also the "Approve and run" button is only for deploying the dev app, it doesn't affect any of the other checks. You can check what other PRs look like, e.g. #24931

@hilts-vaughan
Copy link
Contributor Author

I swapped the watch to "All Activity" just now.

@crisbeto
Copy link
Member

Then I really don't know why it didn't run 😕. Anyway, I ran it locally and the only problem is this lint failure:

src/material/tabs/paginated-tab-header.ts:258:3
ERROR: 258:3  jsdoc-format  jsdoc is not formatted correctly on this line

@hilts-vaughan
Copy link
Contributor Author

I tried fix it locally. Want me to run something else? Should I abandon #24932?

@crisbeto
Copy link
Member

Yeah, you can abandon the other one.

Copy link
Member

@crisbeto crisbeto left a comment

Choose a reason for hiding this comment

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

Caretaker note: for some reason the CI didn't run on this PR. I've verified that everything passes on my machine.

@crisbeto crisbeto added P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent action: merge The PR is ready for merge by the caretaker labels May 17, 2022
@crisbeto crisbeto added merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note target: patch This PR is targeted for the next patch release labels May 17, 2022
@crisbeto crisbeto self-assigned this May 18, 2022
@crisbeto crisbeto merged commit 2ced52a into angular:main May 18, 2022
crisbeto pushed a commit that referenced this pull request May 18, 2022
…tems have changed in dimensions (#24885)

(cherry picked from commit 2ced52a)
crisbeto pushed a commit that referenced this pull request May 18, 2022
…tems have changed in dimensions (#24885)

(cherry picked from commit 2ced52a)
crapStone pushed a commit to Calciumdibromid/CaBr2 that referenced this pull request May 30, 2022
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@angular/cdk](https://github.com/angular/components) | dependencies | patch | [`13.3.7` -> `13.3.8`](https://renovatebot.com/diffs/npm/@angular%2fcdk/13.3.7/13.3.8) |
| [@angular/material](https://github.com/angular/components) | dependencies | patch | [`13.3.7` -> `13.3.8`](https://renovatebot.com/diffs/npm/@angular%2fmaterial/13.3.7/13.3.8) |

---

### Release Notes

<details>
<summary>angular/components</summary>

### [`v13.3.8`](https://github.com/angular/components/blob/HEAD/CHANGELOG.md#&#8203;1338-lead-lamp-2022-05-25)

[Compare Source](angular/components@13.3.7...13.3.8)

##### material

| Commit | Type | Description |
| -- | -- | -- |
| [8611a742b](angular/components@8611a74) | fix | **tabs:** ensure the ink bar realigns when the tab header items have changed in dimensions ([#&#8203;24885](angular/components#24885)) |

##### material-experimental

| Commit | Type | Description |
| -- | -- | -- |
| [7386fe865](angular/components@7386fe8) | fix | **mdc-checkbox:** Use cursor:pointer for label ([#&#8203;24927](angular/components#24927)) |

##### multiple

| Commit | Type | Description |
| -- | -- | -- |
| [a7ee8a80b](angular/components@a7ee8a8) | fix | fix focus and hover styles for mdc-checkbox and mdc-radio ([#&#8203;24930](angular/components#24930)) |
| [b8fddd60c](angular/components@b8fddd6) | fix | fix style imports and deps for mdc-checkbox and mdc-radio ([#&#8203;24972](angular/components#24972)) |

#### Special Thanks

Joey Perrott, Miles Malerba, Vaughan Hilts and Wagner Maciel

<!-- CHANGELOG SPLIT MARKER -->

</details>

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Co-authored-by: cabr2-bot <cabr2.help@gmail.com>
Reviewed-on: https://codeberg.org/Calciumdibromid/CaBr2/pulls/1381
Reviewed-by: crapStone <crapstone@noreply.codeberg.org>
Co-authored-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
Co-committed-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Jun 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Tabs] Ink bar does not update when container resizes [Tabs] Ink bar is not correctly aligned on init
2 participants