ElementUI

Foreword

One of my goals for the year 24 was to engage with the open-source community, and getting started can be challenging. I chose VueCore and ElementUI as my starting points, submitting a PR to VueCore which, unfortunately, addressed an issue that had been fixed a week prior, leaving it without follow-up.

However, an interesting issue arose in ElementUI, let’s delve into the details:

The Problem

The issue looked like this: Sharp Teeth

Here is also an SFC link: SFC

The bug occurs when there are numerous menu items in the component; ElementUI condenses the lengthy menu into an expandable section:

As shown in the above figure, hovering over or clicking on the ellipsis area reveals an expansion panel containing all menu items.

The issue arises when clicking the expansion panel: if there are too many menu items, the length of the expansion panel becomes excessively long, causing the page to extend vertically, introducing a scrollbar and compressing the view width of the content below.

While the compression may be expected behavior, since the expansion panel naturally expands to accommodate more menu items, the problem lies in a throttling function:

const getIndexPath = (index: string) => subMenus.value[index].indexPath;

// Common computer monitor FPS is 60Hz, implying 60 redraws per second. Calculation formula: 1000ms/60 ≈ 16.67ms. To avoid potential repeated triggering during `resize`, set wait to 16.67 * 2 = 33.34ms.
const debounce = (fn: () => void, wait = 33.34) => {
  let timmer: ReturnType<typeof setTimeout> | null;
  return () => {
    timmer && clearTimeout(timmer);
    timmer = setTimeout(() => {
      fn();
    }, wait);
  };
};

let isFirstTimeRender = true;
const handleResize = () => {
  if (sliceIndex.value === calcSliceIndex()) return;
  const callback = () => {
    sliceIndex.value = -1;
    nextTick(() => {
      sliceIndex.value = calcSliceIndex();
    });
  };

  // Execute the callback directly during the first resize event to avoid shaking.
  isFirstTimeRender ? callback() : debounce(callback)();
  isFirstTimeRender = false;
};

This function is triggered when the viewport width changes, executing handleResize. This function calculates the current menu length and invokes a debounced function, which executes the callback only once within a specified time frame. The callback recalculates the menu length and assigns it to sliceIndex, thus determining the expansion state of the expansion panel.

The amusingly problematic part: the scrollbar introduced triggers the throttled function so frequently that it causes the page to jitter. Since the throttling delay is 33.34ms and the scrollbar trigger frequency is around 16.67ms, the throttled function is repeatedly called, leading to the jittering effect.

Solution

To solve this issue:

Initially, I considered modifying the SCSS styles, but since the style adjusts based on the menu length, changing the styles wasn’t a viable solution.

I then realized that the throttling function’s purpose was likely to address the destruction and recreation of the component during window resizing. Was it necessary to invoke this function so frequently? Could we trigger it only when the number of components changes?

So, I added a line to the original code:

if (sliceIndex.value === calcSliceIndex()) return;

Although this issue was relatively simple, I found it intriguing enough to document. Submitting this PR allowed me to review Git operations and experience the pull request process and standards in an open-source project, which was indeed enlightening.

Next milestone: Aim to submit PRs to Vue and ElementUI, and hopefully, contribute to the core team!