Skip to content

Feature/priority navigation #573

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

Merged
merged 45 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0418368
setup resizeObserver
JesmoDev Sep 8, 2023
f56113d
hide tabs that overflow
JesmoDev Sep 8, 2023
70c57ba
unset display value on visible tabs
JesmoDev Sep 8, 2023
7441e64
add css var for tab horizontal padding
JesmoDev Sep 8, 2023
c2ae4c5
Merge branch 'feature/popover-container' into feature/priority-naviga…
JesmoDev Sep 8, 2023
8edead8
package lock
JesmoDev Sep 8, 2023
882afd4
add popover
JesmoDev Sep 11, 2023
5d44fa6
Merge branch 'feature/popover-container' into feature/priority-naviga…
JesmoDev Sep 11, 2023
7544944
move styles to the bottom of the file
JesmoDev Sep 11, 2023
9fd31bc
work
JesmoDev Sep 11, 2023
27d5730
hide tabs
JesmoDev Sep 11, 2023
e221a5e
cleanup
JesmoDev Sep 11, 2023
9960eb0
show if active tab inside dropdown
JesmoDev Sep 11, 2023
254294e
add label
JesmoDev Sep 11, 2023
2ca6850
more labels
JesmoDev Sep 11, 2023
1fb1f86
link proxies
JesmoDev Sep 13, 2023
2e9cdad
dropdown styling
JesmoDev Sep 13, 2023
f28c72c
clone tab node instead of factory function
JesmoDev Sep 13, 2023
73f86ec
add comment
JesmoDev Sep 13, 2023
1ae74f2
Merge branch 'feature/popover-v2' into feature/priority-navigation
JesmoDev Sep 13, 2023
dfcf9e0
Merge remote-tracking branch 'origin/v1/contrib' into feature/priorit…
JesmoDev Sep 14, 2023
747d345
add navigation dropdown direction
JesmoDev Sep 14, 2023
c63ede5
makes tab items able to be 100% width by overwriting host width
JesmoDev Sep 14, 2023
0bdcb2a
add activeBarLocation so that the active bar can be on all sides
JesmoDev Sep 14, 2023
6582637
make hidden tabs 100% width
JesmoDev Sep 14, 2023
4795ed7
rename
JesmoDev Sep 14, 2023
aa7d4b7
set dropdown active bar location
JesmoDev Sep 14, 2023
4fd8606
cleanup
JesmoDev Sep 14, 2023
2bf332d
cleanup
JesmoDev Sep 15, 2023
2c13b37
optimize so updates only occur when necessary
JesmoDev Sep 15, 2023
55cebc0
move tab styles to the bottom
JesmoDev Sep 15, 2023
52cdb4e
css var for tab text alignment
JesmoDev Sep 15, 2023
f41a4e7
change default active bar placement
JesmoDev Sep 15, 2023
32d367a
update story
JesmoDev Sep 15, 2023
c34a49f
cleanup
JesmoDev Sep 15, 2023
326b937
move styles back to the top for a cleaner PR
JesmoDev Sep 15, 2023
e3062d5
cleanup
JesmoDev Sep 15, 2023
fed1228
move styles back to the top for a cleaner PR
JesmoDev Sep 15, 2023
8a54891
cleanup
JesmoDev Sep 15, 2023
aaccab3
cleanup
JesmoDev Sep 15, 2023
a785350
cleanup
JesmoDev Sep 18, 2023
761c35c
remove priority navigation property
JesmoDev Sep 18, 2023
d7f1f2b
vertical orientation
nielslyngsoe Sep 18, 2023
cc0e9fc
overflow: hidden;
nielslyngsoe Sep 18, 2023
4e347ce
remove story
nielslyngsoe Sep 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cSpell.words": ["combobox", "Umbraco"]
"cSpell.words": ["combobox", "cssprop", "noopener", "noreferrer", "Umbraco"]
}
251 changes: 231 additions & 20 deletions packages/uui-tabs/lib/uui-tab-group.element.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { css, html, LitElement } from 'lit';
import { queryAssignedElements } from 'lit/decorators.js';
import { property, query, queryAssignedElements } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';

import type { UUIButtonElement } from '@umbraco-ui/uui-button/lib';
import '@umbraco-ui/uui-button/lib/uui-button.element';
import '@umbraco-ui/uui-popover-container/lib/uui-popover-container.element';
import '@umbraco-ui/uui-symbol-more/lib/uui-symbol-more.element';

import { UUITabElement } from './uui-tab.element';

Expand All @@ -24,58 +30,263 @@ export class UUITabGroupElement extends LitElement {
::slotted(*:not(:last-of-type)) {
border-right: 1px solid var(--uui-tab-divider, none);
}

.hidden-tab {
width: 100%;
}

#hidden-tabs-container {
width: fit-content;
display: flex;
flex-direction: column;
background: var(--uui-color-surface);
border-radius: var(--uui-border-radius);
box-shadow: var(--uui-shadow-depth-3);
overflow: hidden;
}
:host([dropdown-direction='horizontal']) #hidden-tabs-container {
flex-direction: row;
}

#more-button {
margin-left: auto;
position: relative;
}
#more-button::before {
content: '';
position: absolute;
bottom: 0;
width: 100%;
background-color: var(--uui-color-current);
height: 0px;
border-radius: 3px 3px 0 0;
opacity: 0;
transition: opacity ease-in 120ms, height ease-in 120ms;
}
#more-button.active-inside::before {
opacity: 1;
height: 4px;
transition: opacity 120ms, height ease-out 120ms;
}
`,
];

@query('#more-button')
private _moreButtonElement!: UUIButtonElement;

@queryAssignedElements({
flatten: true,
selector: 'uui-tab, [uui-tab], [role=tab]',
})
private _slottedNodes?: HTMLElement[];
private _tabElements: HTMLElement[] = [];

private _setTabArray() {
this._tabElements = this._slottedNodes ? this._slottedNodes : [];
/**
* Set the flex direction of the content of the dropdown.
* @type {string}
* @attr
* @default vertical
*/
@property({
type: String,
reflect: true,
attribute: 'dropdown-content-direction',
})
dropdownContentDirection: 'vertical' | 'horizontal' = 'vertical';

#tabElements: HTMLElement[] = [];

#hiddenTabElements: UUITabElement[] = [];
#hiddenTabElementsMap: Map<UUITabElement, UUITabElement> = new Map();

#visibilityBreakpoints: number[] = [];
#oldBreakpoint = 0;

#resizeObserver: ResizeObserver = new ResizeObserver(
this.#onResize.bind(this)
);

connectedCallback() {
super.connectedCallback();
this.#resizeObserver.observe(this);
if (!this.hasAttribute('role')) this.setAttribute('role', 'tablist');
}

disconnectedCallback() {
super.disconnectedCallback();
this.#resizeObserver.unobserve(this);
}

private _onSlotChange() {
this._tabElements.forEach(el => {
el.removeEventListener('click', this._onTabClicked);
#onResize(entries: ResizeObserverEntry[]) {
this.#updateCollapsibleTabs(entries[0].contentBoxSize[0].inlineSize);
}

#onSlotChange() {
this.#tabElements.forEach(el => {
el.removeEventListener('click', this.#onTabClicked);
});

this._setTabArray();
this.#setTabArray();

this._tabElements.forEach(el => {
el.addEventListener('click', this._onTabClicked);
this.#tabElements.forEach(el => {
el.addEventListener('click', this.#onTabClicked);
});
}

private _onTabClicked = (e: MouseEvent) => {
#onTabClicked = (e: MouseEvent) => {
const selectedElement = e.currentTarget as HTMLElement;
if (this._elementIsTabLike(selectedElement)) {
if (this.#isElementTabLike(selectedElement)) {
selectedElement.active = true;
const linkedElement = this.#hiddenTabElementsMap.get(selectedElement);

if (linkedElement) {
linkedElement.active = true;
}

const filtered = this._tabElements.filter(el => el !== selectedElement);
// Reset all other tabs
const filtered = [
...this.#tabElements,
...this.#hiddenTabElements,
].filter(el => el !== selectedElement && el !== linkedElement);

filtered.forEach(el => {
if (this._elementIsTabLike(el)) {
if (this.#isElementTabLike(el)) {
el.active = false;
}
});

// Check if there are any active tabs in the dropdown
const hasActiveHidden = this.#hiddenTabElements.some(
el => el.active && el !== linkedElement
);

hasActiveHidden
? this._moreButtonElement.classList.add('active-inside')
: this._moreButtonElement.classList.remove('active-inside');
}
};

private _elementIsTabLike(el: any): el is UUITabElement {
return el instanceof UUITabElement || 'active' in el;
#updateCollapsibleTabs(containerWidth: number) {
const buttonWidth = this._moreButtonElement.offsetWidth;

// Only update if the container is smaller than the last breakpoint
if (
this.#visibilityBreakpoints.slice(-1)[0] < containerWidth &&
this.#hiddenTabElements.length === 0
)
return;

// Only update if the new breakpoint is different from the old one
let newBreakpoint = Number.MAX_VALUE;

for (let i = this.#visibilityBreakpoints.length - 1; i > -1; i--) {
const breakpoint = this.#visibilityBreakpoints[i];
// 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) {
newBreakpoint = i;
break;
}
}

if (newBreakpoint === this.#oldBreakpoint) return;
this.#oldBreakpoint = newBreakpoint;

// Do the update
// Reset the hidden tabs
this.#hiddenTabElements.forEach(el => {
el.removeEventListener('click', this.#onTabClicked);
});
this.#hiddenTabElements = [];
this.#hiddenTabElementsMap.clear();

let hasActiveTabInDropdown = false;

for (let i = 0; i < this.#visibilityBreakpoints.length; 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) {
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;
proxyTab.addEventListener('click', this.#onTabClicked);
proxyTab.classList.add('hidden-tab');
proxyTab.style.display = '';
proxyTab.orientation = this.dropdownContentDirection;

// Link the proxy tab to the original tab
this.#hiddenTabElementsMap.set(proxyTab, tab);
this.#hiddenTabElementsMap.set(tab, proxyTab);

this.#hiddenTabElements.push(proxyTab);

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

hasActiveTabInDropdown
? this._moreButtonElement.classList.add('active-inside')
: this._moreButtonElement.classList.remove('active-inside');

this.requestUpdate();
}

connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute('role')) this.setAttribute('role', 'tablist');
#calculateBreakPoints() {
// Whenever a tab is added or removed, we need to recalculate the breakpoints
let childrenWidth = 0;

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

this.#updateCollapsibleTabs(this.offsetWidth);
}

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

#isElementTabLike(el: any): el is UUITabElement {
return el instanceof UUITabElement || 'active' in el;
}

render() {
return html` <slot @slotchange=${this._onSlotChange}></slot> `;
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>
<uui-popover-container
id="popover-container"
popover
margin="10"
placement="bottom-end">
<div id="hidden-tabs-container">
${repeat(this.#hiddenTabElements, el => html`${el}`)}
</div>
</uui-popover-container>
`;
}
}

Expand Down
Loading