vuematerial/vue-material

Responsive Tabs

pongstylin opened this issue · 0 comments

Your template asked me to be thoughtful. I hope you don't mind it being lengthy as well. I'm considering this a feature request, but it does speak to some undesirable behavior if developers don't handle it explicitly.

  • What will it allow you to do that you can't do today?
    Enjoy responsive "md-tabs" elements without hacks.

  • How will it make current work-arounds straightforward?
    By having responsive "md-tabs" elements by default.

  • What potential bugs and edge cases does it help to avoid?
    Overflowing the viewport (or at least, document) width, by default.
    Having an "md-tabs" element width greater than widest tab content, by default.

Use Case
In my component, I have a fairly vanilla implementation of tabs:

<template>
  <div>
    <MdContent>
      <MdTabs ref="tabs">
        <MdTab md-label="Users">
          <UserList />
        </MdTab>
        <MdTab md-label="Videos">
          <VideoList />
        </MdTab>
      </MdTabs>
    </MdContent>
  </div>
</template>

The component is inside of a responsive container. That is, no explicit width has been defined for the entire HTML ancestry. This is not responsive, however, because the md-tabs element width will be at least the SUM of the width of each md-tab inside of it. In other words, if the window or viewport is less than the sum of the width of each md-tab, then it will overflow and be clipped.

The Sum Problem

This is a side-effect of displaying the content of all tabs at the same time. This is necessary to support the nifty sliding content transition when the user toggles between tabs. Not only is the nifty transition worth keeping, but the simplicity of not having to toggle various styles on the various tabs as they are hidden and shown is nice as well. But keeping all that the same, we can still be responsive by using the width of the tab with the widest content.

The Solution

Now, don't think that we're trying to solve the "overflow window or viewport" problem. That is easily solved by setting a "max-width: 100vw" on the "md-tabs" element. But that just fixes one of the symptoms of the sum width problem. Another symptom can be seen if you style the "md-content" in my example as a flexbox. This prevents the "md-tabs" div from taking up all available width. But it can still take more space than I would like since it is the sum of the tabs' width instead of the max of the tabs' width.

I don't want to dictate the solution, but I am sharing the hack that I did to achieve a solution below. If you can think of a pure CSS way to solve the problem while still allowing for responsive md-tabs width, please provide it. As long as the root md-tabs element is responsive, then the content of each tab has control over its width when an abundance of horizontal space is available.

One caveat with regard to the example solution below. It does the right thing even if the "md-tabs" element is given an explicit width that is smaller than its natural width. In that case, tab content width may shrink appropriately and will be measured at the reduced width. In this case, the tabs container will be given the same width as the "md-tabs" element. BUT, if the "md-tabs" element is resized, then the tabs container width should be removed, recalculated, and reapplied. I do not demonstrate this, but it deserves consideration when thinking about improving on this solution.

Of course, if the solution proves useful in only some situations and detrimental in others, it can be toggled on or off as well.

    mounted() {
      const tabsContent = this.$refs.tabs.$refs.tabsContent;
      const tabs = tabsContent.$children;
      const tabsContainer = tabsContent.$el.childNodes[0];

      this.$nextTick(() => {
        // Hide all tabs
        for (const tab of tabs) {
          tab.$el.style.display = 'none';
        }

        let maxWidth = 0;
        // Show each tab by itself to get a true sense of width
        for (const tab of tabs) {
          tab.$el.style.display = null;
          maxWidth = Math.max(maxWidth, tab.$el.offsetWidth);
          tab.$el.style.display = 'none';
        }

        // Show all tabs
        for (const tab of tabs) {
          tab.$el.style.display = null;
        }

        const tcStyle = window.getComputedStyle(tabsContainer);
        if (tcStyle.boxSizing === 'border-box') {
          const borderWidth = parseFloat(tcStyle.borderLeftWidth) + parseFloat(tcStyle.borderRightWidth);
          const paddingWidth = parseFloat(tcStyle.paddingLeft) + parseFloat(tcStyle.paddingRight);
          tabsContainer.style.width = (borderWidth + paddingWidth + maxWidth) + 'px';
        } else {
          tabsContainer.style.width = maxWidth + 'px';
        }
      });
    },