vira 31.25.0 → 31.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/pop-up/vira-menu-item.element.d.ts +6 -0
- package/dist/elements/pop-up/vira-menu-item.element.js +32 -30
- package/dist/elements/vira-absolute-time.element.d.ts +8 -0
- package/dist/elements/vira-absolute-time.element.js +5 -2
- package/dist/elements/vira-bold-text.element.js +2 -1
- package/dist/elements/vira-collapsible-card.element.js +2 -1
- package/dist/elements/vira-date-input.element.js +1 -0
- package/dist/elements/vira-error.element.js +2 -1
- package/dist/elements/vira-input.element.js +2 -1
- package/dist/elements/vira-json-form.element.d.ts +12 -0
- package/dist/elements/vira-json-form.element.js +104 -3
- package/dist/elements/vira-link.element.js +3 -3
- package/dist/elements/vira-relative-time.element.d.ts +7 -0
- package/dist/elements/vira-relative-time.element.js +8 -1
- package/dist/elements/vira-tabs.element.d.ts +29 -2
- package/dist/elements/vira-tabs.element.js +463 -162
- package/dist/elements/vira-tag.element.js +1 -1
- package/dist/styles/font.d.ts +3 -0
- package/dist/styles/font.js +3 -0
- package/dist/styles/form-styles.d.ts +1 -1
- package/dist/styles/form-styles.js +2 -1
- package/dist/util/vira-json-schema.d.ts +18 -1
- package/dist/util/vira-json-schema.js +37 -1
- package/package.json +1 -1
|
@@ -3,13 +3,15 @@ import { arrayToObject, filterMap, getObjectTypedKeys, } from '@augment-vir/comm
|
|
|
3
3
|
import { ContrastLevelName } from '@electrovir/color/dist/data/contrast/contrast.js';
|
|
4
4
|
import { classMap, css, defineElementEvent, html, listen, nothing, onDomCreated, unsafeCSS, } from 'element-vir';
|
|
5
5
|
import { routeHasPaths, } from 'spa-router-vir';
|
|
6
|
+
import { themeDefaultKey } from 'theme-vir/dist/color-theme/color-theme.js';
|
|
7
|
+
import { Check24Icon } from '../icons/icon-svgs/24/check-24.icon.js';
|
|
6
8
|
import { createFocusStyles } from '../styles/focus.js';
|
|
9
|
+
import { viraFontCssVars } from '../styles/font.js';
|
|
7
10
|
import { viraFormCssVars } from '../styles/form-styles.js';
|
|
8
11
|
import { standaloneThemeColorNames, ViraColorVariant, viraColorVariantToHostClassKey, } from '../styles/form-variants.js';
|
|
9
12
|
import { noNativeFormStyles, noUserSelect, viraDisabledStyles, viraTheme } from '../styles/index.js';
|
|
10
13
|
import { viraThemeByKeys, ViraThemeColorName } from '../styles/vira-color-theme-object.js';
|
|
11
14
|
import { defineViraElement } from '../util/define-vira-element.js';
|
|
12
|
-
import { createOverflowObserver } from '../util/overflow-observer.js';
|
|
13
15
|
import { renderMenuItemEntries } from '../util/pop-up-helpers.js';
|
|
14
16
|
import { ViraMenuTrigger } from './pop-up/vira-menu-trigger.element.js';
|
|
15
17
|
import { ViraMenuCornerStyle } from './pop-up/vira-menu.element.js';
|
|
@@ -41,6 +43,69 @@ export var ViraTabsIconLayout;
|
|
|
41
43
|
/** Icon renders beside the label. */
|
|
42
44
|
ViraTabsIconLayout["Horizontal"] = "horizontal";
|
|
43
45
|
})(ViraTabsIconLayout || (ViraTabsIconLayout = {}));
|
|
46
|
+
function measureElementWidth(mirrorElement, selector) {
|
|
47
|
+
const element = mirrorElement.querySelector(selector);
|
|
48
|
+
return element ? element.getBoundingClientRect().width : 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Groups a flat list of tabs into clusters. Consecutive tabs sharing the same `group` string are
|
|
52
|
+
* merged into a single cluster; ungrouped tabs (and tabs whose `group` differs from their
|
|
53
|
+
* predecessor) each start a new cluster.
|
|
54
|
+
*
|
|
55
|
+
* @category Internal
|
|
56
|
+
*/
|
|
57
|
+
export function buildClusters(tabList) {
|
|
58
|
+
return tabList.reduce((accumulatedClusters, tab) => {
|
|
59
|
+
const lastCluster = accumulatedClusters[accumulatedClusters.length - 1];
|
|
60
|
+
if (tab.group != undefined && lastCluster && lastCluster.group === tab.group) {
|
|
61
|
+
return [
|
|
62
|
+
...accumulatedClusters.slice(0, -1),
|
|
63
|
+
{
|
|
64
|
+
group: lastCluster.group,
|
|
65
|
+
tabs: [
|
|
66
|
+
...lastCluster.tabs,
|
|
67
|
+
tab,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
...accumulatedClusters,
|
|
74
|
+
{
|
|
75
|
+
group: tab.group,
|
|
76
|
+
tabs: [tab],
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}, []);
|
|
80
|
+
}
|
|
81
|
+
function renderGroupedContent(tabList, renderItem) {
|
|
82
|
+
const clusters = buildClusters(tabList);
|
|
83
|
+
return clusters.map((cluster, clusterIndex) => {
|
|
84
|
+
const previousCluster = clusters[clusterIndex - 1];
|
|
85
|
+
const dividerTemplate = previousCluster && previousCluster.group != undefined && cluster.group != undefined
|
|
86
|
+
? html `
|
|
87
|
+
<span class="tab-cluster-divider"></span>
|
|
88
|
+
`
|
|
89
|
+
: nothing;
|
|
90
|
+
const labelTemplate = cluster.group == undefined
|
|
91
|
+
? nothing
|
|
92
|
+
: html `
|
|
93
|
+
<span class="tab-group-label">${cluster.group}</span>
|
|
94
|
+
`;
|
|
95
|
+
return html `
|
|
96
|
+
${dividerTemplate}
|
|
97
|
+
<div
|
|
98
|
+
class=${classMap({
|
|
99
|
+
'tab-cluster': true,
|
|
100
|
+
standalone: cluster.group == undefined,
|
|
101
|
+
})}
|
|
102
|
+
>
|
|
103
|
+
${labelTemplate}
|
|
104
|
+
<div class="tab-cluster-tabs">${cluster.tabs.map(renderItem)}</div>
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
44
109
|
/**
|
|
45
110
|
* A tab bar element that renders an array of tabs with an animated selection indicator.
|
|
46
111
|
*
|
|
@@ -54,7 +119,8 @@ export const ViraTabs = defineViraElement()({
|
|
|
54
119
|
},
|
|
55
120
|
state() {
|
|
56
121
|
return {
|
|
57
|
-
|
|
122
|
+
/** How many of the visible tabs are collapsed into the overflow "more" menu. */
|
|
123
|
+
overflowCount: 0,
|
|
58
124
|
/** A callback to remove all internal observers. */
|
|
59
125
|
cleanupObserver: undefined,
|
|
60
126
|
};
|
|
@@ -89,15 +155,14 @@ export const ViraTabs = defineViraElement()({
|
|
|
89
155
|
}),
|
|
90
156
|
'vira-tabs-icon-layout-vertical': ({ inputs }) => !inputs.iconLayout || inputs.iconLayout === ViraTabsIconLayout.Vertical,
|
|
91
157
|
'vira-tabs-icon-layout-horizontal': ({ inputs }) => inputs.iconLayout === ViraTabsIconLayout.Horizontal,
|
|
92
|
-
'vira-tabs-overflowing': ({ state }) => state.isOverflowing,
|
|
93
158
|
'vira-tabs-fill-width': ({ inputs }) => !!inputs.shouldFillWidth,
|
|
94
159
|
},
|
|
95
160
|
cssVars: {
|
|
96
161
|
'vira-tabs-active-color': viraThemeByKeys[viraColorVariantToHostClassKey[ViraColorVariant.Info]]['behind-bg'][ContrastLevelName.NonBodyText].background.value,
|
|
97
162
|
'vira-tabs-active-hover-color': viraThemeByKeys[viraColorVariantToHostClassKey[ViraColorVariant.Info]]['behind-bg'][ContrastLevelName.Header].background.value,
|
|
98
|
-
'vira-tabs-inactive-color': viraTheme.colors[
|
|
99
|
-
'vira-tabs-inactive-hover-color': viraTheme.colors[
|
|
100
|
-
'vira-tabs-bar-thickness': '
|
|
163
|
+
'vira-tabs-inactive-color': viraTheme.colors[themeDefaultKey].foreground.value,
|
|
164
|
+
'vira-tabs-inactive-hover-color': viraTheme.colors[themeDefaultKey].foreground.value,
|
|
165
|
+
'vira-tabs-bar-thickness': '2px',
|
|
101
166
|
},
|
|
102
167
|
styles: ({ hostClasses, cssVars }) => {
|
|
103
168
|
function buildThemedTabsColors(colorName) {
|
|
@@ -146,17 +211,26 @@ export const ViraTabs = defineViraElement()({
|
|
|
146
211
|
box-sizing: border-box;
|
|
147
212
|
${noUserSelect};
|
|
148
213
|
width: 100%;
|
|
214
|
+
/** Tab labels, the overflow trigger, and menu items never wrap to a second line. */
|
|
215
|
+
white-space: nowrap;
|
|
149
216
|
}
|
|
150
217
|
|
|
151
218
|
.tabs-container {
|
|
152
219
|
display: flex;
|
|
153
220
|
position: relative;
|
|
154
|
-
list-style: none;
|
|
155
|
-
overflow: hidden;
|
|
156
221
|
margin: 0;
|
|
157
222
|
padding: 0;
|
|
158
223
|
}
|
|
159
224
|
|
|
225
|
+
.tabs-measure {
|
|
226
|
+
position: absolute;
|
|
227
|
+
top: 0;
|
|
228
|
+
left: 0;
|
|
229
|
+
overflow: visible;
|
|
230
|
+
visibility: hidden;
|
|
231
|
+
pointer-events: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
160
234
|
${hostClasses['vira-tabs-bar-top'].selector},
|
|
161
235
|
${hostClasses['vira-tabs-bar-bottom'].selector} {
|
|
162
236
|
& .tabs-container {
|
|
@@ -171,7 +245,7 @@ export const ViraTabs = defineViraElement()({
|
|
|
171
245
|
}
|
|
172
246
|
}
|
|
173
247
|
|
|
174
|
-
|
|
248
|
+
.tab-item {
|
|
175
249
|
${noNativeFormStyles};
|
|
176
250
|
cursor: pointer;
|
|
177
251
|
display: flex;
|
|
@@ -180,7 +254,8 @@ export const ViraTabs = defineViraElement()({
|
|
|
180
254
|
text-align: center;
|
|
181
255
|
position: relative;
|
|
182
256
|
color: ${cssVars['vira-tabs-inactive-color'].value};
|
|
183
|
-
font-size: ${viraFormCssVars['vira-form-
|
|
257
|
+
font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
|
|
258
|
+
font-weight: ${viraFontCssVars['vira-font-weight-medium'].value};
|
|
184
259
|
text-decoration: none;
|
|
185
260
|
${createFocusStyles({
|
|
186
261
|
renderInside: true,
|
|
@@ -217,13 +292,12 @@ export const ViraTabs = defineViraElement()({
|
|
|
217
292
|
border-bottom: 1px solid
|
|
218
293
|
${viraTheme.colors['vira-grey-foreground-decoration'].foreground.value};
|
|
219
294
|
|
|
220
|
-
&
|
|
295
|
+
& .tab-item::after {
|
|
221
296
|
bottom: 0;
|
|
222
297
|
left: 0;
|
|
223
298
|
right: 0;
|
|
224
299
|
height: ${cssVars['vira-tabs-bar-thickness'].value};
|
|
225
|
-
border-radius:
|
|
226
|
-
${cssVars['vira-tabs-bar-thickness'].value} 0 0;
|
|
300
|
+
border-radius: 0;
|
|
227
301
|
}
|
|
228
302
|
}
|
|
229
303
|
|
|
@@ -231,13 +305,12 @@ export const ViraTabs = defineViraElement()({
|
|
|
231
305
|
border-top: 1px solid
|
|
232
306
|
${viraTheme.colors['vira-grey-foreground-decoration'].foreground.value};
|
|
233
307
|
|
|
234
|
-
&
|
|
308
|
+
& .tab-item::after {
|
|
235
309
|
top: 0;
|
|
236
310
|
left: 0;
|
|
237
311
|
right: 0;
|
|
238
312
|
height: ${cssVars['vira-tabs-bar-thickness'].value};
|
|
239
|
-
border-radius: 0
|
|
240
|
-
${cssVars['vira-tabs-bar-thickness'].value};
|
|
313
|
+
border-radius: 0;
|
|
241
314
|
}
|
|
242
315
|
}
|
|
243
316
|
|
|
@@ -245,13 +318,12 @@ export const ViraTabs = defineViraElement()({
|
|
|
245
318
|
border-left: 1px solid
|
|
246
319
|
${viraTheme.colors['vira-grey-foreground-decoration'].foreground.value};
|
|
247
320
|
|
|
248
|
-
&
|
|
321
|
+
& .tab-item::after {
|
|
249
322
|
top: 0;
|
|
250
323
|
bottom: 0;
|
|
251
324
|
left: 0;
|
|
252
325
|
width: ${cssVars['vira-tabs-bar-thickness'].value};
|
|
253
|
-
border-radius: 0
|
|
254
|
-
${cssVars['vira-tabs-bar-thickness'].value} 0;
|
|
326
|
+
border-radius: 0;
|
|
255
327
|
}
|
|
256
328
|
}
|
|
257
329
|
|
|
@@ -259,13 +331,12 @@ export const ViraTabs = defineViraElement()({
|
|
|
259
331
|
border-right: 1px solid
|
|
260
332
|
${viraTheme.colors['vira-grey-foreground-decoration'].foreground.value};
|
|
261
333
|
|
|
262
|
-
&
|
|
334
|
+
& .tab-item::after {
|
|
263
335
|
top: 0;
|
|
264
336
|
bottom: 0;
|
|
265
337
|
right: 0;
|
|
266
338
|
width: ${cssVars['vira-tabs-bar-thickness'].value};
|
|
267
|
-
border-radius:
|
|
268
|
-
${cssVars['vira-tabs-bar-thickness'].value};
|
|
339
|
+
border-radius: 0;
|
|
269
340
|
}
|
|
270
341
|
}
|
|
271
342
|
|
|
@@ -291,32 +362,16 @@ export const ViraTabs = defineViraElement()({
|
|
|
291
362
|
}
|
|
292
363
|
}
|
|
293
364
|
|
|
294
|
-
${hostClasses['vira-tabs-overflowing'].selector} .tabs-container {
|
|
295
|
-
visibility: hidden;
|
|
296
|
-
height: 0;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
.overflow-menu {
|
|
300
|
-
display: none;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
${hostClasses['vira-tabs-overflowing'].selector} .overflow-menu {
|
|
304
|
-
display: flex;
|
|
305
|
-
align-items: center;
|
|
306
|
-
width: fit-content;
|
|
307
|
-
padding-left: 8px;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
365
|
${hostClasses['vira-tabs-fill-width'].selector} {
|
|
311
366
|
& .tabs-container {
|
|
312
367
|
flex-grow: 1;
|
|
313
368
|
}
|
|
314
369
|
|
|
315
|
-
&
|
|
370
|
+
& .tab-item {
|
|
316
371
|
flex-grow: 1;
|
|
317
372
|
}
|
|
318
373
|
|
|
319
|
-
& .
|
|
374
|
+
& .tab-item ${ViraLink} {
|
|
320
375
|
flex-grow: 1;
|
|
321
376
|
justify-content: center;
|
|
322
377
|
}
|
|
@@ -326,18 +381,50 @@ export const ViraTabs = defineViraElement()({
|
|
|
326
381
|
text-decoration: none;
|
|
327
382
|
}
|
|
328
383
|
|
|
329
|
-
.
|
|
384
|
+
.tab-item ${ViraLink} {
|
|
330
385
|
display: flex;
|
|
331
386
|
padding: 8px 16px;
|
|
332
387
|
}
|
|
333
388
|
|
|
334
|
-
|
|
335
|
-
|
|
389
|
+
.tab-more {
|
|
390
|
+
display: flex;
|
|
391
|
+
flex-shrink: 0;
|
|
392
|
+
align-items: center;
|
|
393
|
+
|
|
394
|
+
& ${ViraButton} {
|
|
395
|
+
flex-shrink: 0;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.tabs-container.grouped {
|
|
400
|
+
align-items: flex-end;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.tab-cluster {
|
|
404
|
+
display: flex;
|
|
405
|
+
flex-direction: column;
|
|
406
|
+
justify-content: flex-end;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.tab-cluster-tabs {
|
|
410
|
+
display: flex;
|
|
411
|
+
flex-direction: row;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.tab-group-label {
|
|
415
|
+
font-size: 11px;
|
|
416
|
+
font-weight: ${viraFontCssVars['vira-font-weight-medium'].value};
|
|
417
|
+
padding: 8px 16px 4px;
|
|
418
|
+
color: ${viraTheme.colors['vira-grey-foreground-header'].foreground.value};
|
|
336
419
|
}
|
|
337
420
|
|
|
338
|
-
.
|
|
421
|
+
.tab-cluster-divider {
|
|
339
422
|
flex-shrink: 0;
|
|
340
|
-
|
|
423
|
+
align-self: stretch;
|
|
424
|
+
width: 1px;
|
|
425
|
+
margin: 8px 0;
|
|
426
|
+
background-color: ${viraTheme.colors['vira-grey-foreground-decoration'].foreground
|
|
427
|
+
.value};
|
|
341
428
|
}
|
|
342
429
|
`;
|
|
343
430
|
},
|
|
@@ -345,85 +432,22 @@ export const ViraTabs = defineViraElement()({
|
|
|
345
432
|
state.cleanupObserver?.();
|
|
346
433
|
},
|
|
347
434
|
render({ inputs, state, updateState, host, dispatch, events }) {
|
|
348
|
-
|
|
349
|
-
if (tab.isHidden) {
|
|
350
|
-
return undefined;
|
|
351
|
-
}
|
|
435
|
+
function renderTabInner(tab, forMeasurement) {
|
|
352
436
|
const isSelected = routeHasPaths(inputs.currentRoute, tab.paths, {
|
|
353
437
|
exactMatch: tab.exactMatch,
|
|
354
438
|
});
|
|
355
439
|
const iconTemplate = tab.icon
|
|
356
440
|
? html `
|
|
357
|
-
|
|
441
|
+
<${ViraIcon.assign({
|
|
358
442
|
icon: tab.icon,
|
|
359
443
|
})}></${ViraIcon}>
|
|
360
|
-
|
|
444
|
+
`
|
|
361
445
|
: nothing;
|
|
362
446
|
const isInert = isSelected || !!tab.isDisabled;
|
|
363
|
-
return html `
|
|
364
|
-
<li
|
|
365
|
-
class=${classMap({
|
|
366
|
-
selected: isSelected,
|
|
367
|
-
disabled: !!tab.isDisabled,
|
|
368
|
-
})}
|
|
369
|
-
role="presentation"
|
|
370
|
-
${listen('click', () => {
|
|
371
|
-
if (!tab.isDisabled) {
|
|
372
|
-
dispatch(new events.tabSelect(tab));
|
|
373
|
-
}
|
|
374
|
-
})}
|
|
375
|
-
>
|
|
376
|
-
<${ViraLink.assign({
|
|
377
|
-
route: {
|
|
378
|
-
router: inputs.router,
|
|
379
|
-
route: {
|
|
380
|
-
paths: tab.paths.fullPaths,
|
|
381
|
-
},
|
|
382
|
-
scrollToTop: true,
|
|
383
|
-
},
|
|
384
|
-
disableLinkStyles: true,
|
|
385
|
-
attributePassthrough: {
|
|
386
|
-
a: {
|
|
387
|
-
role: 'tab',
|
|
388
|
-
'aria-selected': String(isSelected),
|
|
389
|
-
'aria-disabled': String(!!tab.isDisabled),
|
|
390
|
-
tabindex: isInert ? '-1' : undefined,
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
})}>
|
|
394
|
-
<span class="tab-content">
|
|
395
|
-
${iconTemplate}
|
|
396
|
-
<${ViraBoldText.assign({
|
|
397
|
-
text: tab.label,
|
|
398
|
-
bold: isSelected,
|
|
399
|
-
})}
|
|
400
|
-
class="tab-label"
|
|
401
|
-
></${ViraBoldText}>
|
|
402
|
-
</span>
|
|
403
|
-
</${ViraLink}>
|
|
404
|
-
</li>
|
|
405
|
-
`;
|
|
406
|
-
}, check.isTruthy);
|
|
407
|
-
const selectedTab = inputs.tabs.find((tab) => routeHasPaths(inputs.currentRoute, tab.paths, {
|
|
408
|
-
exactMatch: tab.exactMatch,
|
|
409
|
-
}));
|
|
410
|
-
const menuItems = renderMenuItemEntries(filterMap(inputs.tabs, (tab) => {
|
|
411
|
-
if (tab.isHidden) {
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
const isSelected = routeHasPaths(inputs.currentRoute, tab.paths, {
|
|
415
|
-
exactMatch: tab.exactMatch,
|
|
416
|
-
});
|
|
417
|
-
const iconTemplate = tab.icon
|
|
418
|
-
? html `
|
|
419
|
-
<${ViraIcon.assign({
|
|
420
|
-
icon: tab.icon,
|
|
421
|
-
})}></${ViraIcon}>
|
|
422
|
-
`
|
|
423
|
-
: nothing;
|
|
424
447
|
return {
|
|
448
|
+
isSelected,
|
|
425
449
|
content: html `
|
|
426
|
-
|
|
450
|
+
<${ViraLink.assign({
|
|
427
451
|
route: {
|
|
428
452
|
router: inputs.router,
|
|
429
453
|
route: {
|
|
@@ -432,65 +456,342 @@ export const ViraTabs = defineViraElement()({
|
|
|
432
456
|
scrollToTop: true,
|
|
433
457
|
},
|
|
434
458
|
disableLinkStyles: true,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
459
|
+
attributePassthrough: forMeasurement
|
|
460
|
+
? undefined
|
|
461
|
+
: {
|
|
462
|
+
a: {
|
|
463
|
+
role: 'tab',
|
|
464
|
+
'aria-selected': String(isSelected),
|
|
465
|
+
'aria-disabled': String(!!tab.isDisabled),
|
|
466
|
+
tabindex: isInert ? '-1' : undefined,
|
|
467
|
+
},
|
|
468
|
+
},
|
|
442
469
|
})}>
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
470
|
+
<span class="tab-content">
|
|
471
|
+
${iconTemplate}
|
|
472
|
+
<${ViraBoldText.assign({
|
|
473
|
+
text: tab.label,
|
|
474
|
+
bold: isSelected,
|
|
475
|
+
})}
|
|
476
|
+
class="tab-label"
|
|
477
|
+
></${ViraBoldText}>
|
|
478
|
+
</span>
|
|
479
|
+
</${ViraLink}>
|
|
480
|
+
`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function handleTabClick(tab) {
|
|
484
|
+
if (!tab.isDisabled) {
|
|
485
|
+
dispatch(new events.tabSelect(tab));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function renderTabItem(tab) {
|
|
489
|
+
const { isSelected, content } = renderTabInner(tab, false);
|
|
490
|
+
return html `
|
|
491
|
+
<div
|
|
492
|
+
class=${classMap({
|
|
493
|
+
'tab-item': true,
|
|
446
494
|
selected: isSelected,
|
|
447
|
-
disabled: tab.isDisabled,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
495
|
+
disabled: !!tab.isDisabled,
|
|
496
|
+
})}
|
|
497
|
+
role="presentation"
|
|
498
|
+
${listen('click', () => handleTabClick(tab))}
|
|
499
|
+
>
|
|
500
|
+
${content}
|
|
501
|
+
</div>
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
function renderMeasureTabItem(tab) {
|
|
505
|
+
const { isSelected, content } = renderTabInner(tab, true);
|
|
506
|
+
return html `
|
|
507
|
+
<div
|
|
508
|
+
class=${classMap({
|
|
509
|
+
'tab-item': true,
|
|
510
|
+
'measure-selected': isSelected,
|
|
511
|
+
})}
|
|
512
|
+
>
|
|
513
|
+
${content}
|
|
514
|
+
</div>
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
function buildMenuItems(tabList) {
|
|
518
|
+
return renderMenuItemEntries(filterMap(tabList, (tab) => {
|
|
519
|
+
const isSelected = routeHasPaths(inputs.currentRoute, tab.paths, {
|
|
520
|
+
exactMatch: tab.exactMatch,
|
|
521
|
+
});
|
|
522
|
+
const iconTemplate = tab.icon
|
|
523
|
+
? html `
|
|
524
|
+
<${ViraIcon.assign({
|
|
525
|
+
icon: tab.icon,
|
|
526
|
+
})}></${ViraIcon}>
|
|
527
|
+
`
|
|
528
|
+
: nothing;
|
|
529
|
+
return {
|
|
530
|
+
content: html `
|
|
531
|
+
<${ViraLink.assign({
|
|
532
|
+
route: {
|
|
533
|
+
router: inputs.router,
|
|
534
|
+
route: {
|
|
535
|
+
paths: tab.paths.fullPaths,
|
|
536
|
+
},
|
|
537
|
+
scrollToTop: true,
|
|
538
|
+
},
|
|
539
|
+
disableLinkStyles: true,
|
|
540
|
+
stylePassthrough: {
|
|
541
|
+
a: css `
|
|
542
|
+
display: flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
gap: 8px;
|
|
545
|
+
`,
|
|
546
|
+
},
|
|
547
|
+
})}>
|
|
548
|
+
${iconTemplate} ${tab.label}
|
|
549
|
+
</${ViraLink}>
|
|
550
|
+
`,
|
|
551
|
+
selected: isSelected,
|
|
552
|
+
disabled: tab.isDisabled,
|
|
553
|
+
onClick() {
|
|
554
|
+
handleTabClick(tab);
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}, check.isTruthy));
|
|
558
|
+
}
|
|
559
|
+
function measureOverflow(mirrorElement) {
|
|
560
|
+
const availableWidth = host.clientWidth;
|
|
561
|
+
if (!availableWidth) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Vertical (left/right) tab bars stack their tabs in a column, so horizontal
|
|
566
|
+
* width-based overflow does not apply: keep every tab inline.
|
|
567
|
+
*/
|
|
568
|
+
const isVertical = globalThis
|
|
569
|
+
.getComputedStyle(mirrorElement)
|
|
570
|
+
.flexDirection.startsWith('column');
|
|
571
|
+
if (isVertical) {
|
|
572
|
+
if (state.overflowCount !== 0) {
|
|
573
|
+
updateState({
|
|
574
|
+
overflowCount: 0,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const tabElements = Array.from(mirrorElement.querySelectorAll('.tab-item'));
|
|
580
|
+
const tabCount = tabElements.length;
|
|
581
|
+
if (!tabCount) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const tabWidths = tabElements.map((element) => element.getBoundingClientRect().width);
|
|
585
|
+
/**
|
|
586
|
+
* In grouped mode a partially-shown cluster still renders its group label and its
|
|
587
|
+
* leading divider, so its inline width is `max(labelWidth, shownTabsWidth)` rather than
|
|
588
|
+
* a bare sum of tab widths. Capture each tab's cluster geometry (in tab order) so the
|
|
589
|
+
* inline width for any leading count can be derived from what is actually rendered.
|
|
590
|
+
*/
|
|
591
|
+
const clusterElements = Array.from(mirrorElement.querySelectorAll('.tab-cluster'));
|
|
592
|
+
const containerLeft = mirrorElement.getBoundingClientRect().left;
|
|
593
|
+
const tabClusterInfo = clusterElements.flatMap((clusterElement) => {
|
|
594
|
+
const clusterLeft = clusterElement.getBoundingClientRect().left - containerLeft;
|
|
595
|
+
const labelElement = clusterElement.querySelector('.tab-group-label');
|
|
596
|
+
const clusterLabelWidth = labelElement
|
|
597
|
+
? labelElement.getBoundingClientRect().width
|
|
598
|
+
: 0;
|
|
599
|
+
return Array.from(clusterElement.querySelectorAll('.tab-item')).map((tabElement, indexInCluster) => {
|
|
600
|
+
return {
|
|
601
|
+
clusterLeft,
|
|
602
|
+
clusterLabelWidth,
|
|
603
|
+
indexInCluster,
|
|
604
|
+
width: tabElement.getBoundingClientRect().width,
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
/** Width of the inline area when the first `count` tabs are shown. */
|
|
609
|
+
function inlineWidthForCount(count) {
|
|
610
|
+
if (count <= 0) {
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
else if (!clusterElements.length) {
|
|
614
|
+
return tabWidths.slice(0, count).reduce((sum, width) => sum + width, 0);
|
|
615
|
+
}
|
|
616
|
+
const lastInfo = tabClusterInfo[count - 1];
|
|
617
|
+
if (!lastInfo) {
|
|
618
|
+
return 0;
|
|
619
|
+
}
|
|
620
|
+
const inlineTabsInLastCluster = tabClusterInfo
|
|
621
|
+
.slice(count - 1 - lastInfo.indexInCluster, count)
|
|
622
|
+
.reduce((sum, info) => sum + info.width, 0);
|
|
623
|
+
return (lastInfo.clusterLeft +
|
|
624
|
+
Math.max(lastInfo.clusterLabelWidth, inlineTabsInLastCluster));
|
|
625
|
+
}
|
|
626
|
+
const totalWidth = inlineWidthForCount(tabCount);
|
|
627
|
+
/**
|
|
628
|
+
* Largest number of leading tabs whose inline width fits within the available width
|
|
629
|
+
* minus the given reserve (the width the "more" button will occupy). Can return 0.
|
|
630
|
+
*/
|
|
631
|
+
function maxInlineForReserve(reservePx) {
|
|
632
|
+
const limit = availableWidth - reservePx;
|
|
633
|
+
return tabElements.reduce((fittingCount, _tabElement, index) => inlineWidthForCount(index + 1) <= limit ? index + 1 : fittingCount, 0);
|
|
634
|
+
}
|
|
635
|
+
/** When every tab fits on its own, there is no overflow and no "more" button. */
|
|
636
|
+
if (totalWidth <= availableWidth) {
|
|
637
|
+
if (state.overflowCount !== 0) {
|
|
638
|
+
updateState({
|
|
639
|
+
overflowCount: 0,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const defaultButtonWidth = measureElementWidth(mirrorElement, '.measure-more-default');
|
|
645
|
+
const labeledButtonWidth = measureElementWidth(mirrorElement, '.measure-more-labeled');
|
|
646
|
+
const selectedElement = mirrorElement.querySelector('.tab-item.measure-selected');
|
|
647
|
+
const selectedIndex = selectedElement ? tabElements.indexOf(selectedElement) : -1;
|
|
648
|
+
/**
|
|
649
|
+
* Reserving the default ("more") trigger reveals whether the selected tab would end up
|
|
650
|
+
* collapsed.
|
|
651
|
+
*/
|
|
652
|
+
const defaultInlineCount = maxInlineForReserve(defaultButtonWidth);
|
|
653
|
+
const selectedIsCollapsed = selectedIndex >= 0 && selectedIndex >= defaultInlineCount;
|
|
654
|
+
/**
|
|
655
|
+
* When the selected tab is collapsed, the trigger always shows that tab's own label
|
|
656
|
+
* (with a checkmark), reserving its width even when it cannot fully fit (it is allowed
|
|
657
|
+
* to overflow rather than hide which tab is selected). Otherwise the trigger shows the
|
|
658
|
+
* plain default overflow label.
|
|
659
|
+
*/
|
|
660
|
+
const inlineCount = selectedIsCollapsed
|
|
661
|
+
? maxInlineForReserve(labeledButtonWidth)
|
|
662
|
+
: defaultInlineCount;
|
|
663
|
+
const newOverflowCount = Math.max(1, tabCount - inlineCount);
|
|
664
|
+
if (newOverflowCount !== state.overflowCount) {
|
|
665
|
+
updateState({
|
|
666
|
+
overflowCount: newOverflowCount,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function setUpOverflowMeasurement(mirrorElement) {
|
|
671
|
+
state.cleanupObserver?.();
|
|
672
|
+
let frameId = undefined;
|
|
673
|
+
function scheduleMeasure() {
|
|
674
|
+
if (frameId != undefined) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
frameId = requestAnimationFrame(() => {
|
|
678
|
+
frameId = undefined;
|
|
679
|
+
measureOverflow(mirrorElement);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const resizeObserver = new ResizeObserver(scheduleMeasure);
|
|
683
|
+
resizeObserver.observe(host);
|
|
684
|
+
/**
|
|
685
|
+
* Observing the (out-of-flow) measurement mirror catches late layout of its async
|
|
686
|
+
* `ViraButton` children, whose real widths drive the overflow math.
|
|
687
|
+
*/
|
|
688
|
+
resizeObserver.observe(mirrorElement);
|
|
689
|
+
const mutationObserver = new MutationObserver(scheduleMeasure);
|
|
690
|
+
mutationObserver.observe(mirrorElement, {
|
|
691
|
+
childList: true,
|
|
692
|
+
subtree: true,
|
|
693
|
+
characterData: true,
|
|
694
|
+
});
|
|
695
|
+
scheduleMeasure();
|
|
696
|
+
updateState({
|
|
697
|
+
cleanupObserver() {
|
|
698
|
+
if (frameId != undefined) {
|
|
699
|
+
cancelAnimationFrame(frameId);
|
|
451
700
|
}
|
|
701
|
+
resizeObserver.disconnect();
|
|
702
|
+
mutationObserver.disconnect();
|
|
452
703
|
},
|
|
453
|
-
};
|
|
454
|
-
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
const visibleTabs = inputs.tabs.filter((tab) => !tab.isHidden);
|
|
707
|
+
const hasGroups = visibleTabs.some((tab) => tab.group != undefined);
|
|
708
|
+
const overflowCount = Math.min(visibleTabs.length, Math.max(0, state.overflowCount));
|
|
709
|
+
const inlineCount = visibleTabs.length - overflowCount;
|
|
710
|
+
const inlineTabs = visibleTabs.slice(0, inlineCount);
|
|
711
|
+
const overflowTabs = visibleTabs.slice(inlineCount);
|
|
712
|
+
const selectedIndex = visibleTabs.findIndex((tab) => routeHasPaths(inputs.currentRoute, tab.paths, {
|
|
713
|
+
exactMatch: tab.exactMatch,
|
|
714
|
+
}));
|
|
715
|
+
const selectedTab = selectedIndex >= 0 ? visibleTabs[selectedIndex] : undefined;
|
|
716
|
+
/** Whether the selected tab is currently collapsed into the overflow menu. */
|
|
717
|
+
const selectedIsCollapsed = selectedIndex >= 0 && selectedIndex >= inlineCount;
|
|
718
|
+
const inlineContent = hasGroups
|
|
719
|
+
? renderGroupedContent(inlineTabs, renderTabItem)
|
|
720
|
+
: inlineTabs.map(renderTabItem);
|
|
721
|
+
const measureContent = hasGroups
|
|
722
|
+
? renderGroupedContent(visibleTabs, renderMeasureTabItem)
|
|
723
|
+
: visibleTabs.map(renderMeasureTabItem);
|
|
724
|
+
const overflowLabel = inputs.overflowLabel || 'More';
|
|
725
|
+
/**
|
|
726
|
+
* Hidden copies of the "more" button in each of its possible shapes, rendered inside the
|
|
727
|
+
* measurement mirror so their true widths can be measured before choosing which one fits:
|
|
728
|
+
* the default overflow label, and the collapsed selected tab's own label (with a
|
|
729
|
+
* checkmark).
|
|
730
|
+
*/
|
|
731
|
+
const measureMoreButtons = html `
|
|
732
|
+
<${ViraButton.assign({
|
|
733
|
+
text: overflowLabel,
|
|
734
|
+
showMenuCaret: true,
|
|
735
|
+
color: ViraColorVariant.Neutral,
|
|
736
|
+
})}
|
|
737
|
+
class="measure-more-default"
|
|
738
|
+
></${ViraButton}>
|
|
739
|
+
${selectedTab
|
|
740
|
+
? html `
|
|
741
|
+
<${ViraButton.assign({
|
|
742
|
+
text: selectedTab.label,
|
|
743
|
+
icon: Check24Icon,
|
|
744
|
+
showMenuCaret: true,
|
|
745
|
+
color: ViraColorVariant.Neutral,
|
|
746
|
+
})}
|
|
747
|
+
class="measure-more-labeled"
|
|
748
|
+
></${ViraButton}>
|
|
749
|
+
`
|
|
750
|
+
: nothing}
|
|
751
|
+
`;
|
|
752
|
+
const overflowMenuTemplate = overflowTabs.length
|
|
753
|
+
? html `
|
|
754
|
+
<div class="tab-more">
|
|
755
|
+
<${ViraMenuTrigger.assign({
|
|
756
|
+
horizontalAnchor: inputs.menuHorizontalAnchor,
|
|
757
|
+
isDisabled: inputs.menuIsDisabled,
|
|
758
|
+
popUpOffset: inputs.menuPopUpOffset,
|
|
759
|
+
menuCornerStyle: ViraMenuCornerStyle.AllRounded,
|
|
760
|
+
})}>
|
|
761
|
+
<${ViraButton.assign({
|
|
762
|
+
text: selectedIsCollapsed ? selectedTab?.label : overflowLabel,
|
|
763
|
+
icon: selectedIsCollapsed ? Check24Icon : undefined,
|
|
764
|
+
showMenuCaret: true,
|
|
765
|
+
color: ViraColorVariant.Neutral,
|
|
766
|
+
})}
|
|
767
|
+
slot=${ViraMenuTrigger.slotNames['vira-menu-trigger-trigger']}
|
|
768
|
+
></${ViraButton}>
|
|
769
|
+
${buildMenuItems(overflowTabs)}
|
|
770
|
+
</${ViraMenuTrigger}>
|
|
771
|
+
</div>
|
|
772
|
+
`
|
|
773
|
+
: nothing;
|
|
455
774
|
return html `
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
775
|
+
<div
|
|
776
|
+
class=${classMap({
|
|
777
|
+
'tabs-container': true,
|
|
778
|
+
'tabs-measure': true,
|
|
779
|
+
grouped: hasGroups,
|
|
461
780
|
})}
|
|
462
|
-
|
|
781
|
+
aria-hidden="true"
|
|
782
|
+
${onDomCreated(setUpOverflowMeasurement)}
|
|
463
783
|
>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
784
|
+
${measureContent} ${measureMoreButtons}
|
|
785
|
+
</div>
|
|
786
|
+
<div
|
|
787
|
+
class=${classMap({
|
|
788
|
+
'tabs-container': true,
|
|
789
|
+
grouped: hasGroups,
|
|
468
790
|
})}
|
|
469
|
-
slot=${ViraMenuTrigger.slotNames['vira-menu-trigger-trigger']}
|
|
470
|
-
></${ViraButton}>
|
|
471
|
-
${menuItems}
|
|
472
|
-
</${ViraMenuTrigger}>
|
|
473
|
-
<ul
|
|
474
|
-
class="tabs-container"
|
|
475
791
|
role="tablist"
|
|
476
|
-
${onDomCreated((tabsElement) => {
|
|
477
|
-
state.cleanupObserver?.();
|
|
478
|
-
updateState({
|
|
479
|
-
cleanupObserver: createOverflowObserver({
|
|
480
|
-
element: tabsElement,
|
|
481
|
-
widthElement: host,
|
|
482
|
-
hysteresisPx: 16,
|
|
483
|
-
onChange(isOverflowing) {
|
|
484
|
-
updateState({
|
|
485
|
-
isOverflowing,
|
|
486
|
-
});
|
|
487
|
-
},
|
|
488
|
-
}),
|
|
489
|
-
});
|
|
490
|
-
})}
|
|
491
792
|
>
|
|
492
|
-
${
|
|
493
|
-
</
|
|
793
|
+
${inlineContent} ${overflowMenuTemplate}
|
|
794
|
+
</div>
|
|
494
795
|
`;
|
|
495
796
|
},
|
|
496
797
|
});
|