1
1
import { defineElement } from '@umbraco-ui/uui-base/lib/registration' ;
2
2
import { css , html , LitElement } from 'lit' ;
3
- import { queryAssignedElements } from 'lit/decorators.js' ;
3
+ import { property , query , queryAssignedElements } from 'lit/decorators.js' ;
4
+ import { repeat } from 'lit/directives/repeat.js' ;
5
+
6
+ import type { UUIButtonElement } from '@umbraco-ui/uui-button/lib' ;
7
+ import '@umbraco-ui/uui-button/lib/uui-button.element' ;
8
+ import '@umbraco-ui/uui-popover-container/lib/uui-popover-container.element' ;
9
+ import '@umbraco-ui/uui-symbol-more/lib/uui-symbol-more.element' ;
4
10
5
11
import { UUITabElement } from './uui-tab.element' ;
6
12
@@ -24,58 +30,263 @@ export class UUITabGroupElement extends LitElement {
24
30
::slotted(*:not(:last-of-type)) {
25
31
border-right: 1px solid var(--uui-tab-divider, none);
26
32
}
33
+
34
+ .hidden-tab {
35
+ width: 100%;
36
+ }
37
+
38
+ #hidden-tabs-container {
39
+ width: fit-content;
40
+ display: flex;
41
+ flex-direction: column;
42
+ background: var(--uui-color-surface);
43
+ border-radius: var(--uui-border-radius);
44
+ box-shadow: var(--uui-shadow-depth-3);
45
+ overflow: hidden;
46
+ }
47
+ :host([dropdown-direction='horizontal']) #hidden-tabs-container {
48
+ flex-direction: row;
49
+ }
50
+
51
+ #more-button {
52
+ margin-left: auto;
53
+ position: relative;
54
+ }
55
+ #more-button::before {
56
+ content: '';
57
+ position: absolute;
58
+ bottom: 0;
59
+ width: 100%;
60
+ background-color: var(--uui-color-current);
61
+ height: 0px;
62
+ border-radius: 3px 3px 0 0;
63
+ opacity: 0;
64
+ transition: opacity ease-in 120ms, height ease-in 120ms;
65
+ }
66
+ #more-button.active-inside::before {
67
+ opacity: 1;
68
+ height: 4px;
69
+ transition: opacity 120ms, height ease-out 120ms;
70
+ }
27
71
` ,
28
72
] ;
29
73
74
+ @query ( '#more-button' )
75
+ private _moreButtonElement ! : UUIButtonElement ;
76
+
30
77
@queryAssignedElements ( {
31
78
flatten : true ,
32
79
selector : 'uui-tab, [uui-tab], [role=tab]' ,
33
80
} )
34
81
private _slottedNodes ?: HTMLElement [ ] ;
35
- private _tabElements : HTMLElement [ ] = [ ] ;
36
82
37
- private _setTabArray ( ) {
38
- this . _tabElements = this . _slottedNodes ? this . _slottedNodes : [ ] ;
83
+ /**
84
+ * Set the flex direction of the content of the dropdown.
85
+ * @type {string }
86
+ * @attr
87
+ * @default vertical
88
+ */
89
+ @property ( {
90
+ type : String ,
91
+ reflect : true ,
92
+ attribute : 'dropdown-content-direction' ,
93
+ } )
94
+ dropdownContentDirection : 'vertical' | 'horizontal' = 'vertical' ;
95
+
96
+ #tabElements: HTMLElement [ ] = [ ] ;
97
+
98
+ #hiddenTabElements: UUITabElement [ ] = [ ] ;
99
+ #hiddenTabElementsMap: Map < UUITabElement , UUITabElement > = new Map ( ) ;
100
+
101
+ #visibilityBreakpoints: number [ ] = [ ] ;
102
+ #oldBreakpoint = 0 ;
103
+
104
+ #resizeObserver: ResizeObserver = new ResizeObserver (
105
+ this . #onResize. bind ( this )
106
+ ) ;
107
+
108
+ connectedCallback ( ) {
109
+ super . connectedCallback ( ) ;
110
+ this . #resizeObserver. observe ( this ) ;
111
+ if ( ! this . hasAttribute ( 'role' ) ) this . setAttribute ( 'role' , 'tablist' ) ;
112
+ }
113
+
114
+ disconnectedCallback ( ) {
115
+ super . disconnectedCallback ( ) ;
116
+ this . #resizeObserver. unobserve ( this ) ;
39
117
}
40
118
41
- private _onSlotChange ( ) {
42
- this . _tabElements . forEach ( el => {
43
- el . removeEventListener ( 'click' , this . _onTabClicked ) ;
119
+ #onResize( entries : ResizeObserverEntry [ ] ) {
120
+ this . #updateCollapsibleTabs( entries [ 0 ] . contentBoxSize [ 0 ] . inlineSize ) ;
121
+ }
122
+
123
+ #onSlotChange( ) {
124
+ this . #tabElements. forEach ( el => {
125
+ el . removeEventListener ( 'click' , this . #onTabClicked) ;
44
126
} ) ;
45
127
46
- this . _setTabArray ( ) ;
128
+ this . #setTabArray ( ) ;
47
129
48
- this . _tabElements . forEach ( el => {
49
- el . addEventListener ( 'click' , this . _onTabClicked ) ;
130
+ this . #tabElements . forEach ( el => {
131
+ el . addEventListener ( 'click' , this . #onTabClicked ) ;
50
132
} ) ;
51
133
}
52
134
53
- private _onTabClicked = ( e : MouseEvent ) => {
135
+ #onTabClicked = ( e : MouseEvent ) => {
54
136
const selectedElement = e . currentTarget as HTMLElement ;
55
- if ( this . _elementIsTabLike ( selectedElement ) ) {
137
+ if ( this . #isElementTabLike ( selectedElement ) ) {
56
138
selectedElement . active = true ;
139
+ const linkedElement = this . #hiddenTabElementsMap. get ( selectedElement ) ;
140
+
141
+ if ( linkedElement ) {
142
+ linkedElement . active = true ;
143
+ }
57
144
58
- const filtered = this . _tabElements . filter ( el => el !== selectedElement ) ;
145
+ // Reset all other tabs
146
+ const filtered = [
147
+ ...this . #tabElements,
148
+ ...this . #hiddenTabElements,
149
+ ] . filter ( el => el !== selectedElement && el !== linkedElement ) ;
59
150
60
151
filtered . forEach ( el => {
61
- if ( this . _elementIsTabLike ( el ) ) {
152
+ if ( this . #isElementTabLike ( el ) ) {
62
153
el . active = false ;
63
154
}
64
155
} ) ;
156
+
157
+ // Check if there are any active tabs in the dropdown
158
+ const hasActiveHidden = this . #hiddenTabElements. some (
159
+ el => el . active && el !== linkedElement
160
+ ) ;
161
+
162
+ hasActiveHidden
163
+ ? this . _moreButtonElement . classList . add ( 'active-inside' )
164
+ : this . _moreButtonElement . classList . remove ( 'active-inside' ) ;
65
165
}
66
166
} ;
67
167
68
- private _elementIsTabLike ( el : any ) : el is UUITabElement {
69
- return el instanceof UUITabElement || 'active' in el ;
168
+ #updateCollapsibleTabs( containerWidth : number ) {
169
+ const buttonWidth = this . _moreButtonElement . offsetWidth ;
170
+
171
+ // Only update if the container is smaller than the last breakpoint
172
+ if (
173
+ this . #visibilityBreakpoints. slice ( - 1 ) [ 0 ] < containerWidth &&
174
+ this . #hiddenTabElements. length === 0
175
+ )
176
+ return ;
177
+
178
+ // Only update if the new breakpoint is different from the old one
179
+ let newBreakpoint = Number . MAX_VALUE ;
180
+
181
+ for ( let i = this . #visibilityBreakpoints. length - 1 ; i > - 1 ; i -- ) {
182
+ const breakpoint = this . #visibilityBreakpoints[ i ] ;
183
+ // Subtract the button width when we are not at the last breakpoint
184
+ const containerWidthButtonWidth =
185
+ containerWidth -
186
+ ( i !== this . #visibilityBreakpoints. length - 1 ? buttonWidth : 0 ) ;
187
+
188
+ if ( breakpoint < containerWidthButtonWidth ) {
189
+ newBreakpoint = i ;
190
+ break ;
191
+ }
192
+ }
193
+
194
+ if ( newBreakpoint === this . #oldBreakpoint) return ;
195
+ this . #oldBreakpoint = newBreakpoint ;
196
+
197
+ // Do the update
198
+ // Reset the hidden tabs
199
+ this . #hiddenTabElements. forEach ( el => {
200
+ el . removeEventListener ( 'click' , this . #onTabClicked) ;
201
+ } ) ;
202
+ this . #hiddenTabElements = [ ] ;
203
+ this . #hiddenTabElementsMap. clear ( ) ;
204
+
205
+ let hasActiveTabInDropdown = false ;
206
+
207
+ for ( let i = 0 ; i < this . #visibilityBreakpoints. length ; i ++ ) {
208
+ const breakpoint = this . #visibilityBreakpoints[ i ] ;
209
+ const tab = this . #tabElements[ i ] as UUITabElement ;
210
+
211
+ // Subtract the button width when we are not at the last breakpoint
212
+ const containerWidthButtonWidth =
213
+ containerWidth -
214
+ ( i !== this . #visibilityBreakpoints. length - 1 ? buttonWidth : 0 ) ;
215
+
216
+ if ( breakpoint < containerWidthButtonWidth ) {
217
+ tab . style . display = '' ;
218
+ this . _moreButtonElement . style . display = 'none' ;
219
+ } else {
220
+ // Make a proxy tab to put in the hidden tabs container and link it to the original tab
221
+ const proxyTab = tab . cloneNode ( true ) as UUITabElement ;
222
+ proxyTab . addEventListener ( 'click' , this . #onTabClicked) ;
223
+ proxyTab . classList . add ( 'hidden-tab' ) ;
224
+ proxyTab . style . display = '' ;
225
+ proxyTab . orientation = this . dropdownContentDirection ;
226
+
227
+ // Link the proxy tab to the original tab
228
+ this . #hiddenTabElementsMap. set ( proxyTab , tab ) ;
229
+ this . #hiddenTabElementsMap. set ( tab , proxyTab ) ;
230
+
231
+ this . #hiddenTabElements. push ( proxyTab ) ;
232
+
233
+ tab . style . display = 'none' ;
234
+ this . _moreButtonElement . style . display = '' ;
235
+ if ( tab . active ) {
236
+ hasActiveTabInDropdown = true ;
237
+ }
238
+ }
239
+ }
240
+
241
+ hasActiveTabInDropdown
242
+ ? this . _moreButtonElement . classList . add ( 'active-inside' )
243
+ : this . _moreButtonElement . classList . remove ( 'active-inside' ) ;
244
+
245
+ this . requestUpdate ( ) ;
70
246
}
71
247
72
- connectedCallback ( ) {
73
- super . connectedCallback ( ) ;
74
- if ( ! this . hasAttribute ( 'role' ) ) this . setAttribute ( 'role' , 'tablist' ) ;
248
+ #calculateBreakPoints( ) {
249
+ // Whenever a tab is added or removed, we need to recalculate the breakpoints
250
+ let childrenWidth = 0 ;
251
+
252
+ for ( let i = 0 ; i < this . #tabElements. length ; i ++ ) {
253
+ childrenWidth += this . #tabElements[ i ] . offsetWidth ;
254
+ this . #visibilityBreakpoints[ i ] = childrenWidth ;
255
+ }
256
+
257
+ this . #updateCollapsibleTabs( this . offsetWidth ) ;
258
+ }
259
+
260
+ #setTabArray( ) {
261
+ this . #tabElements = this . _slottedNodes ? this . _slottedNodes : [ ] ;
262
+ this . #calculateBreakPoints( ) ;
263
+ }
264
+
265
+ #isElementTabLike( el : any ) : el is UUITabElement {
266
+ return el instanceof UUITabElement || 'active' in el ;
75
267
}
76
268
77
269
render ( ) {
78
- return html ` < slot @slotchange =${ this . _onSlotChange } > </ slot > ` ;
270
+ return html `
271
+ < slot @slotchange =${ this . #onSlotChange} > </ slot >
272
+ < uui-button
273
+ popovertarget ="popover-container "
274
+ style ="display: none "
275
+ id ="more-button "
276
+ label ="More "
277
+ compact >
278
+ < uui-symbol-more > </ uui-symbol-more >
279
+ </ uui-button >
280
+ < uui-popover-container
281
+ id ="popover-container "
282
+ popover
283
+ margin ="10 "
284
+ placement ="bottom-end ">
285
+ < div id ="hidden-tabs-container ">
286
+ ${ repeat ( this . #hiddenTabElements, el => html `${ el } ` ) }
287
+ </ div >
288
+ </ uui-popover-container >
289
+ ` ;
79
290
}
80
291
}
81
292
0 commit comments