9am / 9am.github.io

9am Blog 🕘 ㍡
https://9am.github.io/
MIT License
3 stars 0 forks source link

Transition on an active highlight - it's not as simple as it seems #18

Open 9am opened 7 months ago

9am commented 7 months ago
After fixing a bug, I found that to achieve this on the web takes more effort than I expected. However, the new view transition API offers a straightforward solution.
spotlight hits
9am commented 7 months ago

Table of contents


Preface

A couple of weeks ago, a bug was reported about a React component owned by my team, which is a ToggleButton that works like the native <input type="radio"> group. Except that a colored highlight will follow the active item with a transition effect as the users select.

s-1

The bug is:

The highlight does not resize and reposition when textContent changes happen on the active item.

s-2

Well, the cause is the original implementation of the active highlight:

  1. To make it transition as UE wanted, it needs to be a separate element with the same size and position as the active item.
  2. It renders the new size and position when the active item changes, which will cover most cases but not DOM mutation.

After digging a little bit deeper, I found that a universal solution is harder than I thought.


𐄡

## The problem Let's look at what we're dealing with: > 1. For a `source ` element (A) in any layout on the web page, create a `highlight` element (B) that follows the size and position of (A) no matter how the page changes. > 2. If the `source` switches from (A) to (C), (B) will switch source accordingly. It can be split into 3 questions: 1. **Where to put the `highlight`?** 2. **How to get the new position and size of the `source`?** 3. **When to trigger rerender?**

𐄡

## A simple case To answer those questions, let's start with a simple case. Here we have a horizontal flex layout of items with the same sizes. ![s-3](https://github.com/9am/9am.github.io/assets/1435457/de574f1c-f0ca-4975-a8c9-18624b0c51c5) > 1, Where to put the `highlight`? To make it transition, we can add a new element(or pseudo-elements) in the `container` and make it `position: absolute`, so that changes outside `container` can not affect how it is positioned. ```html

``` > 2, How to get the new position and size of the `source`? Since it's an even-size horizontal layout, we can calculate the `width` of the active item with `100% / var(--item-num)`. *(gap not included)* As for the `position`, just translate across xAxis by `var(--width) * var(--item-active))`. *(gap not included)* > 3, When to trigger rerender? The moment the number of items or the active item changes, we need to update the `highlight`. Put them together: > ![s-4](https://github.com/9am/9am.github.io/assets/1435457/664c1b2f-0fe2-41ce-b983-b4e6f2605464) > > [**live demo**](https://developer.mozilla.org/en-US/play?id=hVnTdmPad0ibkW2POVwu46%2FDMdkzAxhNCqk3SD1FD5rgc1iUAYjARcyOOfHM%2FHuXFPPcLPjzo99zVT7q) > > The example is in plain html/css/javascript to demonstrate the case. For React, there are other problems to solve. **But the solution is not perfect. There are a bunch of edge cases to consider:** ### 1. What if it's a 2-dimensional layout? > s-5 For the `size` & `position`, we need to include `height` and translateY for vertical alignment to make it work. Additionally, the total column number for the layout is mandatory to do the math. ### 2. What if the items have different sizes? > s-6 Apparently, we can no longer get the `size` by the percentage which leaves us no choice but to use `clientWidth` & `clientHeight`. As for the `position`, we can only rely on `getClientBoundingRect()` to calculate the offset position from the item to the container. ### 3. What if the container resized? > ![s-7](https://github.com/9am/9am.github.io/assets/1435457/05a7b529-1b71-433f-9fdc-929c11a5e848) Maybe it's triggered by users or maybe by parent layout changing or resizing of a sibling, anyway. that's possible. So the trigger of rerender should include a `ResizeOberservor` on the `container`. ### 4. What if the active item resized? > ![s-8](https://github.com/9am/9am.github.io/assets/1435457/70c1226f-06e2-4dda-a3d0-de703999ae18) The bug we encountered falls into this category. A lot of things can cause the item to resize: `textContent` changing, Adding or Removing childNodes, or just setting new `width` & `height` directly in the style. So the trigger of rerender should include a `ResizeOberservor` on the `active item`. ### 5. What if the sibling of the active item resized? > ![s-9](https://github.com/9am/9am.github.io/assets/1435457/527f70b3-14d6-42fe-8473-9547494c4227) Yes, it happens, and if the container has a fixed size, it can not be caught by the `ResizeOberservor` on the `container`. ### 6. What if items reordered? > ![s-10](https://github.com/9am/9am.github.io/assets/1435457/f3dc83b6-e7a8-4af9-a872-b0663d117b11) Well, it's not common, to cover those cases we have no choice but to add a `MutationObserver` on the container.

𐄡

## The universal solution After asking those 'what ifs', I found myself end up trying to figure out how the browser layout elements and what triggers the layout change. So a once-for-all solution looks like this: ### The final answers 1. Where to put the `highlight`? > New Element or Pseudo Element. 2. How to get the new position and size of the `source`? > `getClientBoundingRect()`. 3. When to trigger rerender? > `container` resize & mutate > `active item` resize > ![s-11](https://github.com/9am/9am.github.io/assets/1435457/bb796045-6e9e-4d77-b41b-2b38b16ea870) > > [**live demo**](https://developer.mozilla.org/en-US/play?id=H1RisSObxW0e5aJavQdI5iTRBPa9nKV2SPeKcz00BXn0yeChPMTAlhhg%2BCYABo%2BJRWVR7ne67ZS%2BhIy4) use-spotlight And I created a custom React Hooks called [use-spotlight](https://github.com/9am/use-spotlight) for this kind of situation, which involves other problems to solve like the updating of active `refs` won't trigger a rerender, etc. Anyway I managed to do it, but the code gets more complicated. I can't help but wonder: Does it need to be this hard to do a simple effect like this on the web? What is the missing block here?

𐄡

## New option: view transition API Creating an element with the same size and position as another element is easy on the web. An absolute positioned 100% width height child will always follow the source. Why we have this solution above is that **we couldn't do a transition between different elements**. So we need to stick with 1 element and calculate the size and position manually. The `view transition API` came up to smooth the transition experience for the SPA situation. But it definitely can do more. **It brings transition between different elements to the web for the first time**. Check this out: > ![s-12](https://github.com/9am/9am.github.io/assets/1435457/b5d492b3-83d6-426f-971c-2a9025100f19) > > [**codepen**](https://codepen.io/9am/pen/QWYJJEr) Since the 'highlight' is a pseudo, even without the `startViewTransition()`, it will still be working as a 'fallback' version of 'highlight'. That's a wonderful way of **treating 'transition' as an enhancement**. > [!NOTE] > > This is an experimental technology > Check the [Browser compatibility table](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition#browser_compatibility) carefully before using this in production.

𐄡

## Closing thoughts Web development evolved so fast for years and the trend is not slowing down. New things keep coming up about HTML/CSS/JS, frameworks, bundlers, etc... I'm coding in a way totally different from 5 years ago, even 1 year ago. In a way that's a good thing, but the concepts are getting more and more complicated. And one needs to learn a ton of knowledge ahead to start building something or understand what's happening behind the codes. I kind of miss the simplicity of the old-time web. **At the end of the day, it's not technology that holds us back from sharing ideas**. Or maybe I'm just getting old -_-|||
--- > ## @9am 🕘 > * Read more [articles](https://9am.github.io) at [9am.github.io](https://9am.github.io) > * Find other [things](https://www.npmjs.com/search?q=%409am) I built on [GitHub](https://github.com/9am) and [NPM](https://www.npmjs.com/~9am) > * Contact me via [email](mailto:tech.9am@gmail.com) > * [![Creative Commons License](https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/)