Skip to content

Commit 2f07793

Browse files
JesmoDevnielslyngsoejulczka
authored
fix: uui-tab-group support for gap (#712)
* add flex gap story * add missing imports in story * move contents into main to prevent gap from being applied to popover * add gap css var and add it to calculation * fix NaN issues * update story * remove double text-wrap * calculation change * test story * fix test story * add max width * size corrections --------- Co-authored-by: Niels Lyngsø <[email protected]> Co-authored-by: Julia Gru <[email protected]>
1 parent 12feafb commit 2f07793

File tree

3 files changed

+192
-55
lines changed

3 files changed

+192
-55
lines changed

packages/uui-tabs/lib/uui-tab-group.element.ts

+79-49
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { UUITabElement } from './uui-tab.element';
1515
* @cssprop --uui-tab-group-dropdown-tab-text-hover - Define the tab text hover color in the dropdown
1616
* @cssprop --uui-tab-group-dropdown-tab-text-active - Define the tab text active color in the dropdown
1717
* @cssprop --uui-tab-group-dropdown-background - Define the background color of the dropdown
18+
* @cssprop --uui-tab-group-gap - Define the gap between elements dropdown. Only pixel values are valid
1819
*/
1920
@defineElement('uui-tab-group')
2021
export class UUITabGroupElement extends LitElement {
@@ -30,6 +31,9 @@ export class UUITabGroupElement extends LitElement {
3031
})
3132
private _slottedNodes?: HTMLElement[];
3233

34+
/** Stores the current gap used in the breakpoints */
35+
#currentGap = 0;
36+
3337
/**
3438
* Set the flex direction of the content of the dropdown.
3539
* @type {string}
@@ -69,7 +73,16 @@ export class UUITabGroupElement extends LitElement {
6973
}
7074

7175
#onResize(entries: ResizeObserverEntry[]) {
72-
this.#updateCollapsibleTabs(entries[0].contentBoxSize[0].inlineSize);
76+
// Check if the gap css custom property has changed.
77+
const gapCSSVar = Number.parseFloat(
78+
this.style.getPropertyValue('--uui-tab-group-gap')
79+
);
80+
const newGap = Number.isNaN(gapCSSVar) ? 0 : gapCSSVar;
81+
if (newGap !== this.#currentGap) {
82+
this.#calculateBreakPoints();
83+
} else {
84+
this.#updateCollapsibleTabs(entries[0].contentBoxSize[0].inlineSize);
85+
}
7386
}
7487

7588
#onSlotChange() {
@@ -117,13 +130,42 @@ export class UUITabGroupElement extends LitElement {
117130
}
118131
};
119132

133+
async #calculateBreakPoints() {
134+
// Whenever a tab is added or removed, we need to recalculate the breakpoints
135+
136+
await this.updateComplete; // Wait for the tabs to be rendered
137+
const gapCSSVar = Number.parseFloat(
138+
this.style.getPropertyValue('--uui-tab-group-gap')
139+
);
140+
const gap = Number.isNaN(gapCSSVar) ? 0 : gapCSSVar;
141+
this.#currentGap = gap;
142+
let childrenWidth = 0;
143+
144+
for (let i = 0; i < this.#tabElements.length; i++) {
145+
this.#tabElements[i].style.display = '';
146+
childrenWidth += this.#tabElements[i].offsetWidth;
147+
this.#visibilityBreakpoints[i] = childrenWidth;
148+
// Add the gap, which will then be included in the next breakpoint:
149+
childrenWidth += gap;
150+
}
151+
152+
const tolerance = 2;
153+
this.style.maxWidth = childrenWidth - gap + tolerance + 'px';
154+
155+
this.#updateCollapsibleTabs(this.offsetWidth);
156+
}
157+
158+
#setTabArray() {
159+
this.#tabElements = this._slottedNodes ? this._slottedNodes : [];
160+
this.#calculateBreakPoints();
161+
}
162+
120163
#updateCollapsibleTabs(containerWidth: number) {
121-
const buttonWidth = this._moreButtonElement.offsetWidth;
164+
const moreButtonWidth = this._moreButtonElement.offsetWidth;
122165

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

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

136178
let hasActiveTabInDropdown = false;
137179

138-
for (let i = 0; i < this.#visibilityBreakpoints.length; i++) {
180+
const len = this.#visibilityBreakpoints.length;
181+
for (let i = 0; i < len; i++) {
139182
const breakpoint = this.#visibilityBreakpoints[i];
140183
const tab = this.#tabElements[i] as UUITabElement;
141184

142-
// Subtract the button width when we are not at the last breakpoint
143-
const containerWidthButtonWidth =
144-
containerWidth -
145-
(i !== this.#visibilityBreakpoints.length - 1 ? buttonWidth : 0);
146-
147-
if (breakpoint < containerWidthButtonWidth) {
185+
// If breakpoint is smaller than the container width, then show the tab.
186+
// If last breakpoint, then we will use the containerWidth, as we do not want to include the more-button in that calculation.
187+
if (
188+
breakpoint <=
189+
(i === len - 1 ? containerWidth : containerWithoutButtonWidth)
190+
) {
191+
// Show this tab:
148192
tab.style.display = '';
149-
this._moreButtonElement.style.display = 'none';
150193
} else {
151194
// Make a proxy tab to put in the hidden tabs container and link it to the original tab
152195
const proxyTab = tab.cloneNode(true) as UUITabElement;
@@ -162,16 +205,20 @@ export class UUITabGroupElement extends LitElement {
162205
this.#hiddenTabElements.push(proxyTab);
163206

164207
tab.style.display = 'none';
165-
this._moreButtonElement.style.display = '';
166208
if (tab.active) {
167209
hasActiveTabInDropdown = true;
168210
}
169211
}
170212
}
171213

172214
if (this.#hiddenTabElements.length === 0) {
215+
// Hide more button:
216+
this._moreButtonElement.style.display = 'none';
173217
// close the popover
174218
this._popoverContainerElement.hidePopover();
219+
} else {
220+
// Show more button:
221+
this._moreButtonElement.style.display = '';
175222
}
176223

177224
hasActiveTabInDropdown
@@ -181,29 +228,6 @@ export class UUITabGroupElement extends LitElement {
181228
this.requestUpdate();
182229
}
183230

184-
async #calculateBreakPoints() {
185-
// Whenever a tab is added or removed, we need to recalculate the breakpoints
186-
187-
await this.updateComplete; // Wait for the tabs to be rendered
188-
let childrenWidth = 0;
189-
190-
for (let i = 0; i < this.#tabElements.length; i++) {
191-
this.#tabElements[i].style.display = '';
192-
childrenWidth += this.#tabElements[i].offsetWidth;
193-
this.#visibilityBreakpoints[i] = childrenWidth;
194-
}
195-
196-
const tolerance = 2;
197-
this.style.width = childrenWidth + tolerance + 'px';
198-
199-
this.#updateCollapsibleTabs(this.offsetWidth);
200-
}
201-
202-
#setTabArray() {
203-
this.#tabElements = this._slottedNodes ? this._slottedNodes : [];
204-
this.#calculateBreakPoints();
205-
}
206-
207231
#isElementTabLike(el: any): el is UUITabElement {
208232
return (
209233
typeof el === 'object' && 'active' in el && typeof el.active === 'boolean'
@@ -212,15 +236,17 @@ export class UUITabGroupElement extends LitElement {
212236

213237
render() {
214238
return html`
215-
<slot @slotchange=${this.#onSlotChange}></slot>
216-
<uui-button
217-
popovertarget="popover-container"
218-
style="display: none"
219-
id="more-button"
220-
label="More"
221-
compact>
222-
<uui-symbol-more></uui-symbol-more>
223-
</uui-button>
239+
<div id="main">
240+
<slot @slotchange=${this.#onSlotChange}></slot>
241+
<uui-button
242+
popovertarget="popover-container"
243+
style="display: none"
244+
id="more-button"
245+
label="More"
246+
compact>
247+
<uui-symbol-more></uui-symbol-more>
248+
</uui-button>
249+
</div>
224250
<uui-popover-container
225251
id="popover-container"
226252
popover
@@ -235,13 +261,17 @@ export class UUITabGroupElement extends LitElement {
235261
static styles = [
236262
css`
237263
:host {
264+
width: 100%;
265+
}
266+
267+
#main {
238268
display: flex;
239-
flex-wrap: nowrap;
240-
color: var(--uui-tab-text);
241269
height: 100%;
242270
min-height: 48px;
243271
overflow: hidden;
244272
text-wrap: nowrap;
273+
color: var(--uui-tab-text);
274+
gap: var(--uui-tab-group-gap, 0);
245275
}
246276
247277
#popover-container {

packages/uui-tabs/lib/uui-tab.element.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class UUITabElement extends ActiveMixin(LabelMixin('', LitElement)) {
115115
height: 100%;
116116
min-height: var(--uui-size-12);
117117
min-width: 70px;
118-
padding: var(--uui-size-2)
118+
padding: var(--uui-size-3)
119119
var(--uui-tab-padding-horizontal, var(--uui-size-5));
120120
border: none;
121121
font-size: inherit;

packages/uui-tabs/lib/uui-tabs.story.ts

+112-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import '.';
22
import '@umbraco-ui/uui-icon/lib';
33
import '@umbraco-ui/uui-icon-registry-essential/lib';
4+
import '@umbraco-ui/uui-button/lib';
5+
import '@umbraco-ui/uui-popover-container/lib';
6+
import '@umbraco-ui/uui-symbol-more/lib';
7+
import '@umbraco-ui/uui-input/lib';
48

59
import { Story } from '@storybook/web-components';
610
import { html } from 'lit';
@@ -81,7 +85,7 @@ export const Navbar: Story = () => html`
8185
style="
8286
display: flex;
8387
height: 60px;
84-
font-size: 16px;
88+
font-size: var(--uui-type-default-size);
8589
">
8690
<uui-tab-group style="display: flex;">
8791
<uui-tab label="content"> Content </uui-tab>
@@ -100,7 +104,7 @@ export const UsingHref: Story = () => html`
100104
style="
101105
display: flex;
102106
height: 60px;
103-
font-size: 16px;
107+
font-size: var(--uui-type-default-size);
104108
">
105109
<uui-tab-group>
106110
<uui-tab label="content" href="http://www.umbraco.com/#content">
@@ -132,9 +136,29 @@ export const WithIcons: Story = props => html`
132136
<uui-tab-group
133137
dropdown-direction="horizontal"
134138
style="
135-
height: 70px;
136-
font-size: 12px;
139+
font-size: var(--uui-type-small-size);
137140
${props.inlineStyles}">
141+
<uui-tab label="content">
142+
<uui-icon slot="icon" name="document"></uui-icon>
143+
Content
144+
</uui-tab>
145+
<uui-tab active label="packages">
146+
<uui-icon slot="icon" name="settings"></uui-icon>
147+
Packages
148+
</uui-tab>
149+
<uui-tab label="media">
150+
<uui-icon slot="icon" name="picture"></uui-icon>
151+
Media
152+
</uui-tab>
153+
</uui-tab-group>
154+
</div>
155+
</uui-icon-registry-essential>
156+
`;
157+
WithIcons.parameters = {
158+
docs: {
159+
source: {
160+
code: `
161+
<uui-tab-group>
138162
<uui-tab>
139163
<uui-icon slot="icon" name="document"></uui-icon>
140164
Content
@@ -147,11 +171,94 @@ export const WithIcons: Story = props => html`
147171
<uui-icon slot="icon" name="picture"></uui-icon>
148172
Media
149173
</uui-tab>
174+
</uui-tab-group>`,
175+
},
176+
},
177+
};
178+
179+
export const WithGap: Story = props => html`
180+
<h3>Tabs with custom gap</h3>
181+
<uui-icon-registry-essential>
182+
<div style="display: flex;">
183+
<uui-tab-group
184+
dropdown-direction="horizontal"
185+
style="
186+
--uui-tab-group-gap: 180px;
187+
margin: 0 auto;
188+
font-size: var(--uui-type-small-size);
189+
${props.inlineStyles}">
190+
<uui-tab label="content">
191+
<uui-icon slot="icon" name="document"></uui-icon>
192+
Content
193+
</uui-tab>
194+
<uui-tab active label="packages">
195+
<uui-icon slot="icon" name="settings"></uui-icon>
196+
Packages
197+
</uui-tab>
198+
<uui-tab label="media">
199+
<uui-icon slot="icon" name="picture"></uui-icon>
200+
Media
201+
</uui-tab>
150202
</uui-tab-group>
151203
</div>
152204
</uui-icon-registry-essential>
153205
`;
154-
WithIcons.parameters = {
206+
WithGap.parameters = {
207+
docs: {
208+
source: {
209+
code: `
210+
<uui-tab-group>
211+
<uui-tab>
212+
<uui-icon slot="icon" name="document"></uui-icon>
213+
Content
214+
</uui-tab>
215+
<uui-tab active>
216+
<uui-icon slot="icon" name="settings"></uui-icon>
217+
Packages
218+
</uui-tab>
219+
<uui-tab>
220+
<uui-icon slot="icon" name="picture"></uui-icon>
221+
Media
222+
</uui-tab>
223+
</uui-tab-group>`,
224+
},
225+
},
226+
};
227+
228+
export const FlexLayout: Story = props => html`
229+
<h3>Tabs implemented into Flex-box scenario</h3>
230+
<p>
231+
In this case we want the input to grow and the tabs to take up the remaining
232+
space:
233+
</p>
234+
<uui-icon-registry-essential>
235+
<div
236+
style="display: flex; outline: 1px solid black; max-width: 800px; height: 100%; align-items: center; padding-left: 12px;">
237+
<uui-input style="flex-grow: 1; min-width: 200px"></uui-input>
238+
<uui-tab-group
239+
dropdown-direction="horizontal"
240+
style="
241+
flex-grow: 1;
242+
--uui-tab-group-gap: 25px;
243+
font-size: var(--uui-type-small-size);
244+
${props.inlineStyles}">
245+
<uui-tab label="content">
246+
<uui-icon slot="icon" name="document"></uui-icon>
247+
Content
248+
</uui-tab>
249+
<uui-tab active label="packages">
250+
<uui-icon slot="icon" name="settings"></uui-icon>
251+
Packages
252+
</uui-tab>
253+
<uui-tab label="media">
254+
<uui-icon slot="icon" name="picture"></uui-icon>
255+
Media
256+
</uui-tab>
257+
</uui-tab-group>
258+
</div>
259+
</uui-icon-registry-essential>
260+
`;
261+
FlexLayout.parameters = {
155262
docs: {
156263
source: {
157264
code: `

0 commit comments

Comments
 (0)