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

uui-tab-group does not support having a gap between tabs #712

Merged
merged 15 commits into from
Jan 23, 2024
Merged
128 changes: 79 additions & 49 deletions packages/uui-tabs/lib/uui-tab-group.element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { UUITabElement } from './uui-tab.element';
* @cssprop --uui-tab-group-dropdown-tab-text-hover - Define the tab text hover color in the dropdown
* @cssprop --uui-tab-group-dropdown-tab-text-active - Define the tab text active color in the dropdown
* @cssprop --uui-tab-group-dropdown-background - Define the background color of the dropdown
* @cssprop --uui-tab-group-gap - Define the gap between elements dropdown. Only pixel values are valid
*/
@defineElement('uui-tab-group')
export class UUITabGroupElement extends LitElement {
Expand All @@ -30,6 +31,9 @@ export class UUITabGroupElement extends LitElement {
})
private _slottedNodes?: HTMLElement[];

/** Stores the current gap used in the breakpoints */
#currentGap = 0;

/**
* Set the flex direction of the content of the dropdown.
* @type {string}
Expand Down Expand Up @@ -69,7 +73,16 @@ export class UUITabGroupElement extends LitElement {
}

#onResize(entries: ResizeObserverEntry[]) {
this.#updateCollapsibleTabs(entries[0].contentBoxSize[0].inlineSize);
// Check if the gap css custom property has changed.
const gapCSSVar = Number.parseFloat(
this.style.getPropertyValue('--uui-tab-group-gap')
);
const newGap = Number.isNaN(gapCSSVar) ? 0 : gapCSSVar;
if (newGap !== this.#currentGap) {
this.#calculateBreakPoints();
} else {
this.#updateCollapsibleTabs(entries[0].contentBoxSize[0].inlineSize);
}
}

#onSlotChange() {
Expand Down Expand Up @@ -117,13 +130,42 @@ export class UUITabGroupElement extends LitElement {
}
};

async #calculateBreakPoints() {
// Whenever a tab is added or removed, we need to recalculate the breakpoints

await this.updateComplete; // Wait for the tabs to be rendered
const gapCSSVar = Number.parseFloat(
this.style.getPropertyValue('--uui-tab-group-gap')
);
const gap = Number.isNaN(gapCSSVar) ? 0 : gapCSSVar;
this.#currentGap = gap;
let childrenWidth = 0;

for (let i = 0; i < this.#tabElements.length; i++) {
this.#tabElements[i].style.display = '';
childrenWidth += this.#tabElements[i].offsetWidth;
this.#visibilityBreakpoints[i] = childrenWidth;
// Add the gap, which will then be included in the next breakpoint:
childrenWidth += gap;
}

const tolerance = 2;
this.style.maxWidth = childrenWidth - gap + tolerance + 'px';

this.#updateCollapsibleTabs(this.offsetWidth);
}

#setTabArray() {
this.#tabElements = this._slottedNodes ? this._slottedNodes : [];
this.#calculateBreakPoints();
}

#updateCollapsibleTabs(containerWidth: number) {
const buttonWidth = this._moreButtonElement.offsetWidth;
const moreButtonWidth = this._moreButtonElement.offsetWidth;

// Only update if the container is smaller than the last breakpoint
const lastBreakpoint = this.#visibilityBreakpoints.slice(-1)[0];
if (lastBreakpoint < containerWidth && this.#hiddenTabElements.length === 0)
return;
const containerWithoutButtonWidth =
containerWidth -
(moreButtonWidth ? moreButtonWidth + this.#currentGap : 0);

// Do the update
// Reset the hidden tabs
Expand All @@ -135,18 +177,19 @@ export class UUITabGroupElement extends LitElement {

let hasActiveTabInDropdown = false;

for (let i = 0; i < this.#visibilityBreakpoints.length; i++) {
const len = this.#visibilityBreakpoints.length;
for (let i = 0; i < len; i++) {
const breakpoint = this.#visibilityBreakpoints[i];
const tab = this.#tabElements[i] as UUITabElement;

// Subtract the button width when we are not at the last breakpoint
const containerWidthButtonWidth =
containerWidth -
(i !== this.#visibilityBreakpoints.length - 1 ? buttonWidth : 0);

if (breakpoint < containerWidthButtonWidth) {
// If breakpoint is smaller than the container width, then show the tab.
// If last breakpoint, then we will use the containerWidth, as we do not want to include the more-button in that calculation.
if (
breakpoint <=
(i === len - 1 ? containerWidth : containerWithoutButtonWidth)
) {
// Show this tab:
tab.style.display = '';
this._moreButtonElement.style.display = 'none';
} else {
// Make a proxy tab to put in the hidden tabs container and link it to the original tab
const proxyTab = tab.cloneNode(true) as UUITabElement;
Expand All @@ -162,16 +205,20 @@ export class UUITabGroupElement extends LitElement {
this.#hiddenTabElements.push(proxyTab);

tab.style.display = 'none';
this._moreButtonElement.style.display = '';
if (tab.active) {
hasActiveTabInDropdown = true;
}
}
}

if (this.#hiddenTabElements.length === 0) {
// Hide more button:
this._moreButtonElement.style.display = 'none';
// close the popover
this._popoverContainerElement.hidePopover();
} else {
// Show more button:
this._moreButtonElement.style.display = '';
}

hasActiveTabInDropdown
Expand All @@ -181,29 +228,6 @@ export class UUITabGroupElement extends LitElement {
this.requestUpdate();
}

async #calculateBreakPoints() {
// Whenever a tab is added or removed, we need to recalculate the breakpoints

await this.updateComplete; // Wait for the tabs to be rendered
let childrenWidth = 0;

for (let i = 0; i < this.#tabElements.length; i++) {
this.#tabElements[i].style.display = '';
childrenWidth += this.#tabElements[i].offsetWidth;
this.#visibilityBreakpoints[i] = childrenWidth;
}

const tolerance = 2;
this.style.width = childrenWidth + tolerance + 'px';

this.#updateCollapsibleTabs(this.offsetWidth);
}

#setTabArray() {
this.#tabElements = this._slottedNodes ? this._slottedNodes : [];
this.#calculateBreakPoints();
}

#isElementTabLike(el: any): el is UUITabElement {
return (
typeof el === 'object' && 'active' in el && typeof el.active === 'boolean'
Expand All @@ -212,15 +236,17 @@ export class UUITabGroupElement extends LitElement {

render() {
return html`
<slot @slotchange=${this.#onSlotChange}></slot>
<uui-button
popovertarget="popover-container"
style="display: none"
id="more-button"
label="More"
compact>
<uui-symbol-more></uui-symbol-more>
</uui-button>
<div id="main">
<slot @slotchange=${this.#onSlotChange}></slot>
<uui-button
popovertarget="popover-container"
style="display: none"
id="more-button"
label="More"
compact>
<uui-symbol-more></uui-symbol-more>
</uui-button>
</div>
<uui-popover-container
id="popover-container"
popover
Expand All @@ -235,13 +261,17 @@ export class UUITabGroupElement extends LitElement {
static styles = [
css`
:host {
width: 100%;
}

#main {
display: flex;
flex-wrap: nowrap;
color: var(--uui-tab-text);
height: 100%;
min-height: 48px;
overflow: hidden;
text-wrap: nowrap;
color: var(--uui-tab-text);
gap: var(--uui-tab-group-gap, 0);
}

#popover-container {
Expand Down
2 changes: 1 addition & 1 deletion packages/uui-tabs/lib/uui-tab.element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class UUITabElement extends ActiveMixin(LabelMixin('', LitElement)) {
height: 100%;
min-height: var(--uui-size-12);
min-width: 70px;
padding: var(--uui-size-2)
padding: var(--uui-size-3)
var(--uui-tab-padding-horizontal, var(--uui-size-5));
border: none;
font-size: inherit;
Expand Down
117 changes: 112 additions & 5 deletions packages/uui-tabs/lib/uui-tabs.story.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import '.';
import '@umbraco-ui/uui-icon/lib';
import '@umbraco-ui/uui-icon-registry-essential/lib';
import '@umbraco-ui/uui-button/lib';
import '@umbraco-ui/uui-popover-container/lib';
import '@umbraco-ui/uui-symbol-more/lib';
import '@umbraco-ui/uui-input/lib';

import { Story } from '@storybook/web-components';
import { html } from 'lit';
Expand Down Expand Up @@ -81,7 +85,7 @@ export const Navbar: Story = () => html`
style="
display: flex;
height: 60px;
font-size: 16px;
font-size: var(--uui-type-default-size);
">
<uui-tab-group style="display: flex;">
<uui-tab label="content"> Content </uui-tab>
Expand All @@ -100,7 +104,7 @@ export const UsingHref: Story = () => html`
style="
display: flex;
height: 60px;
font-size: 16px;
font-size: var(--uui-type-default-size);
">
<uui-tab-group>
<uui-tab label="content" href="http://www.umbraco.com/#content">
Expand Down Expand Up @@ -132,9 +136,29 @@ export const WithIcons: Story = props => html`
<uui-tab-group
dropdown-direction="horizontal"
style="
height: 70px;
font-size: 12px;
font-size: var(--uui-type-small-size);
${props.inlineStyles}">
<uui-tab label="content">
<uui-icon slot="icon" name="document"></uui-icon>
Content
</uui-tab>
<uui-tab active label="packages">
<uui-icon slot="icon" name="settings"></uui-icon>
Packages
</uui-tab>
<uui-tab label="media">
<uui-icon slot="icon" name="picture"></uui-icon>
Media
</uui-tab>
</uui-tab-group>
</div>
</uui-icon-registry-essential>
`;
WithIcons.parameters = {
docs: {
source: {
code: `
<uui-tab-group>
<uui-tab>
<uui-icon slot="icon" name="document"></uui-icon>
Content
Expand All @@ -147,11 +171,94 @@ export const WithIcons: Story = props => html`
<uui-icon slot="icon" name="picture"></uui-icon>
Media
</uui-tab>
</uui-tab-group>`,
},
},
};

export const WithGap: Story = props => html`
<h3>Tabs with custom gap</h3>
<uui-icon-registry-essential>
<div style="display: flex;">
<uui-tab-group
dropdown-direction="horizontal"
style="
--uui-tab-group-gap: 180px;
margin: 0 auto;
font-size: var(--uui-type-small-size);
${props.inlineStyles}">
<uui-tab label="content">
<uui-icon slot="icon" name="document"></uui-icon>
Content
</uui-tab>
<uui-tab active label="packages">
<uui-icon slot="icon" name="settings"></uui-icon>
Packages
</uui-tab>
<uui-tab label="media">
<uui-icon slot="icon" name="picture"></uui-icon>
Media
</uui-tab>
</uui-tab-group>
</div>
</uui-icon-registry-essential>
`;
WithIcons.parameters = {
WithGap.parameters = {
docs: {
source: {
code: `
<uui-tab-group>
<uui-tab>
<uui-icon slot="icon" name="document"></uui-icon>
Content
</uui-tab>
<uui-tab active>
<uui-icon slot="icon" name="settings"></uui-icon>
Packages
</uui-tab>
<uui-tab>
<uui-icon slot="icon" name="picture"></uui-icon>
Media
</uui-tab>
</uui-tab-group>`,
},
},
};

export const FlexLayout: Story = props => html`
<h3>Tabs implemented into Flex-box scenario</h3>
<p>
In this case we want the input to grow and the tabs to take up the remaining
space:
</p>
<uui-icon-registry-essential>
<div
style="display: flex; outline: 1px solid black; max-width: 800px; height: 100%; align-items: center; padding-left: 12px;">
<uui-input style="flex-grow: 1; min-width: 200px"></uui-input>
<uui-tab-group
dropdown-direction="horizontal"
style="
flex-grow: 1;
--uui-tab-group-gap: 25px;
font-size: var(--uui-type-small-size);
${props.inlineStyles}">
<uui-tab label="content">
<uui-icon slot="icon" name="document"></uui-icon>
Content
</uui-tab>
<uui-tab active label="packages">
<uui-icon slot="icon" name="settings"></uui-icon>
Packages
</uui-tab>
<uui-tab label="media">
<uui-icon slot="icon" name="picture"></uui-icon>
Media
</uui-tab>
</uui-tab-group>
</div>
</uui-icon-registry-essential>
`;
FlexLayout.parameters = {
docs: {
source: {
code: `
Expand Down
Loading