tyrell-components 1.0.0-TC50 → 1.0.0-TC52
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/tyrell.js +1 -1
- package/lib/components/select.js +5 -1
- package/lib/components/selected-tags.d.ts +1 -1
- package/lib/components/selected-tags.js +7 -5
- package/lib/components/tabs.d.ts +1 -0
- package/lib/components/tabs.js +97 -71
- package/lib/styles/select-base.js +9 -0
- package/lib/styles/tab.d.ts +1 -1
- package/lib/styles/tab.js +1 -1
- package/lib/styles/tabs.d.ts +1 -1
- package/lib/styles/tabs.js +113 -26
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/components/select.js
CHANGED
|
@@ -895,7 +895,11 @@ export class TySelect extends TyComponent {
|
|
|
895
895
|
const slot = shadow.querySelector('slot[name="trigger"]');
|
|
896
896
|
if (!stub || !slot)
|
|
897
897
|
return;
|
|
898
|
-
const sync = () =>
|
|
898
|
+
const sync = () => {
|
|
899
|
+
const hasTrigger = slot.assignedElements().length > 0;
|
|
900
|
+
stub.classList.toggle("custom-trigger", hasTrigger);
|
|
901
|
+
this.toggleAttribute("has-trigger", hasTrigger);
|
|
902
|
+
};
|
|
899
903
|
slot.addEventListener("slotchange", sync);
|
|
900
904
|
sync();
|
|
901
905
|
}
|
|
@@ -11,7 +11,7 @@ export declare class TySelectedOptions extends HTMLElement {
|
|
|
11
11
|
private _childObserver;
|
|
12
12
|
private _pickerObserver;
|
|
13
13
|
private _lastTemplate;
|
|
14
|
-
private
|
|
14
|
+
private _pickerOpen;
|
|
15
15
|
private _onPickerOpen;
|
|
16
16
|
private _onPickerClose;
|
|
17
17
|
connectedCallback(): void;
|
|
@@ -12,13 +12,13 @@ export class TySelectedOptions extends HTMLElement {
|
|
|
12
12
|
this._childObserver = null;
|
|
13
13
|
this._pickerObserver = null;
|
|
14
14
|
this._lastTemplate = null;
|
|
15
|
-
this.
|
|
15
|
+
this._pickerOpen = false;
|
|
16
16
|
this._onPickerOpen = () => {
|
|
17
|
-
this.
|
|
18
|
-
this.style.display = 'none';
|
|
17
|
+
this._pickerOpen = true;
|
|
19
18
|
};
|
|
20
19
|
this._onPickerClose = () => {
|
|
21
|
-
this.
|
|
20
|
+
this._pickerOpen = false;
|
|
21
|
+
this.renderChips();
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
static get observedAttributes() {
|
|
@@ -81,7 +81,7 @@ export class TySelectedOptions extends HTMLElement {
|
|
|
81
81
|
this._pickerObserver?.disconnect();
|
|
82
82
|
this._pickerObserver = null;
|
|
83
83
|
this._picker = null;
|
|
84
|
-
this.
|
|
84
|
+
this._pickerOpen = false;
|
|
85
85
|
}
|
|
86
86
|
values() {
|
|
87
87
|
return parseValues(this._picker?.value ?? '');
|
|
@@ -156,6 +156,8 @@ export class TySelectedOptions extends HTMLElement {
|
|
|
156
156
|
return tag;
|
|
157
157
|
}
|
|
158
158
|
renderChips() {
|
|
159
|
+
if (this._pickerOpen)
|
|
160
|
+
return;
|
|
159
161
|
const template = this.template();
|
|
160
162
|
this._lastTemplate = template;
|
|
161
163
|
if (!this._picker) {
|
package/lib/components/tabs.d.ts
CHANGED
package/lib/components/tabs.js
CHANGED
|
@@ -8,6 +8,7 @@ function getTabsAttributes(el) {
|
|
|
8
8
|
height: el.getAttribute('height') || '400px',
|
|
9
9
|
active: el.getAttribute('active'),
|
|
10
10
|
placement: (el.getAttribute('placement') || 'top'),
|
|
11
|
+
fixed: el.hasAttribute('fixed'),
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
function getChildTabs(el) {
|
|
@@ -16,8 +17,11 @@ function getChildTabs(el) {
|
|
|
16
17
|
function getTabId(tab) {
|
|
17
18
|
return tab.getAttribute('id');
|
|
18
19
|
}
|
|
20
|
+
function getSlotLabel(tabsEl, tabId) {
|
|
21
|
+
return tabsEl.querySelector(`:scope > [slot='label-${tabId}']`);
|
|
22
|
+
}
|
|
19
23
|
function hasSlotLabel(tabsEl, tabId) {
|
|
20
|
-
return tabsEl
|
|
24
|
+
return getSlotLabel(tabsEl, tabId) !== null;
|
|
21
25
|
}
|
|
22
26
|
function getTabLabelType(tabsEl, tab) {
|
|
23
27
|
const tabId = getTabId(tab);
|
|
@@ -137,7 +141,7 @@ function updateAriaAttributes(el, shadowRoot, activeId) {
|
|
|
137
141
|
button.setAttribute('aria-selected', String(isActive));
|
|
138
142
|
button.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
139
143
|
button.setAttribute('data-active', String(isActive));
|
|
140
|
-
const slottedLabel = el
|
|
144
|
+
const slottedLabel = getSlotLabel(el, tabId);
|
|
141
145
|
if (slottedLabel) {
|
|
142
146
|
slottedLabel.setAttribute('data-active', String(isActive));
|
|
143
147
|
}
|
|
@@ -213,6 +217,9 @@ function updateActiveTabState(el, tabId, previousId) {
|
|
|
213
217
|
updateAriaAttributes(el, shadowRoot, tabId);
|
|
214
218
|
updatePanelInteraction(el, tabId);
|
|
215
219
|
updateMarker(el, tabId);
|
|
220
|
+
shadowRoot.querySelectorAll('.tab-overflow-item').forEach((item) => {
|
|
221
|
+
item.setAttribute('data-active', String(item.dataset.tabId === tabId));
|
|
222
|
+
});
|
|
216
223
|
const newPanel = tabs[newIndex];
|
|
217
224
|
if (newPanel?.resetScroll) {
|
|
218
225
|
newPanel.resetScroll();
|
|
@@ -236,10 +243,11 @@ function setupResizeObserver(el) {
|
|
|
236
243
|
if (activeIndex !== undefined) {
|
|
237
244
|
updateTransform(el, activeIndex);
|
|
238
245
|
}
|
|
246
|
+
updateOverflow(el);
|
|
239
247
|
if (activeId) {
|
|
240
248
|
updateMarker(el, activeId);
|
|
249
|
+
scrollActiveIntoView(el, activeId, false);
|
|
241
250
|
}
|
|
242
|
-
updateOverflow(el);
|
|
243
251
|
});
|
|
244
252
|
observer.observe(el);
|
|
245
253
|
resizeObservers.set(el, observer);
|
|
@@ -270,16 +278,18 @@ function renderTabButtons(tabsEl, tabs, activeId) {
|
|
|
270
278
|
data-active="${active}"
|
|
271
279
|
${disabled ? 'disabled aria-disabled="true"' : ''}
|
|
272
280
|
>
|
|
273
|
-
${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
|
|
281
|
+
${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
|
|
274
282
|
</button>`;
|
|
275
283
|
}).join('');
|
|
276
284
|
return `
|
|
277
285
|
<div class="tab-buttons" role="tablist" part="buttons-container">
|
|
278
|
-
<div class="
|
|
279
|
-
|
|
280
|
-
|
|
286
|
+
<div class="tab-strip" part="strip">
|
|
287
|
+
<div class="marker-wrapper" part="marker-wrapper">
|
|
288
|
+
${!hasCustomMarker(tabsEl) ? '<div class="default-marker"></div>' : ''}
|
|
289
|
+
<slot name="marker"></slot>
|
|
290
|
+
</div>
|
|
291
|
+
${buttons}
|
|
281
292
|
</div>
|
|
282
|
-
${buttons}
|
|
283
293
|
</div>
|
|
284
294
|
`;
|
|
285
295
|
}
|
|
@@ -302,15 +312,14 @@ function renderButtonsOnly(tabsEl, tabs, activeId) {
|
|
|
302
312
|
data-active="${active}"
|
|
303
313
|
${disabled ? 'disabled aria-disabled="true"' : ''}
|
|
304
314
|
>
|
|
305
|
-
${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
|
|
315
|
+
${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
|
|
306
316
|
</button>`;
|
|
307
317
|
}).join('');
|
|
308
318
|
}
|
|
309
|
-
const OVERFLOW_TRIGGER_WIDTH = 40;
|
|
310
319
|
function getTabMenuLabel(tabsEl, tab) {
|
|
311
320
|
const tabId = getTabId(tab);
|
|
312
321
|
if (tabId && getTabLabelType(tabsEl, tab) === 'slot') {
|
|
313
|
-
const slotted = tabsEl
|
|
322
|
+
const slotted = getSlotLabel(tabsEl, tabId);
|
|
314
323
|
return slotted?.textContent?.trim() || tabId;
|
|
315
324
|
}
|
|
316
325
|
return tab.getAttribute('label') || 'Tab';
|
|
@@ -318,29 +327,28 @@ function getTabMenuLabel(tabsEl, tab) {
|
|
|
318
327
|
function removeOverflowMenu(shadowRoot) {
|
|
319
328
|
shadowRoot.querySelector('.tab-overflow-trigger')?.remove();
|
|
320
329
|
}
|
|
321
|
-
function renderOverflowMenu(el, container,
|
|
322
|
-
|
|
323
|
-
return;
|
|
324
|
-
const tabsById = new Map(allTabs.map((tab) => [getTabId(tab), tab]));
|
|
330
|
+
function renderOverflowMenu(el, container, allTabs) {
|
|
331
|
+
const activeId = getActiveTabId(el, allTabs);
|
|
325
332
|
const trigger = document.createElement('button');
|
|
326
333
|
trigger.className = 'tab-overflow-trigger';
|
|
327
334
|
trigger.type = 'button';
|
|
328
|
-
trigger.setAttribute('aria-label', `${
|
|
335
|
+
trigger.setAttribute('aria-label', `${allTabs.length} tabs`);
|
|
329
336
|
trigger.textContent = '⋯';
|
|
330
337
|
const popup = document.createElement('ty-popup');
|
|
331
338
|
popup.setAttribute('placement', 'bottom');
|
|
332
339
|
const menu = document.createElement('div');
|
|
333
340
|
menu.className = 'tab-overflow-menu';
|
|
334
341
|
menu.setAttribute('role', 'menu');
|
|
335
|
-
|
|
336
|
-
const tabId =
|
|
337
|
-
|
|
338
|
-
if (!tabId || !tab)
|
|
342
|
+
allTabs.forEach((tab) => {
|
|
343
|
+
const tabId = getTabId(tab);
|
|
344
|
+
if (!tabId)
|
|
339
345
|
return;
|
|
340
346
|
const item = document.createElement('button');
|
|
341
347
|
item.type = 'button';
|
|
342
348
|
item.className = 'tab-overflow-item';
|
|
343
349
|
item.setAttribute('role', 'menuitem');
|
|
350
|
+
item.setAttribute('data-tab-id', tabId);
|
|
351
|
+
item.setAttribute('data-active', String(tabId === activeId));
|
|
344
352
|
item.textContent = getTabMenuLabel(el, tab);
|
|
345
353
|
if (isTabDisabled(tab)) {
|
|
346
354
|
item.disabled = true;
|
|
@@ -362,54 +370,65 @@ function updateOverflow(el) {
|
|
|
362
370
|
if (!shadowRoot)
|
|
363
371
|
return;
|
|
364
372
|
const container = shadowRoot.querySelector('.tab-buttons');
|
|
365
|
-
|
|
373
|
+
const strip = shadowRoot.querySelector('.tab-strip');
|
|
374
|
+
if (!container || !strip)
|
|
366
375
|
return;
|
|
376
|
+
if (getTabsAttributes(el).fixed) {
|
|
377
|
+
removeOverflowMenu(shadowRoot);
|
|
378
|
+
strip.classList.add('unclipped');
|
|
379
|
+
strip.style.removeProperty('--fade-left');
|
|
380
|
+
strip.style.removeProperty('--fade-right');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const savedScrollLeft = strip.scrollLeft;
|
|
367
384
|
removeOverflowMenu(shadowRoot);
|
|
368
|
-
|
|
369
|
-
buttons.forEach((b) => b.classList.remove('overflow-hidden'));
|
|
370
|
-
if (buttons.length === 0)
|
|
385
|
+
if (strip.clientWidth <= 0)
|
|
371
386
|
return;
|
|
372
|
-
const
|
|
373
|
-
|
|
387
|
+
const buttons = strip.querySelectorAll('.tab-button');
|
|
388
|
+
const last = buttons[buttons.length - 1];
|
|
389
|
+
const contentWidth = last ? last.offsetLeft + last.offsetWidth : 0;
|
|
390
|
+
const stripWidth = strip.clientWidth;
|
|
391
|
+
const hidden = contentWidth - stripWidth;
|
|
392
|
+
strip.classList.toggle('unclipped', hidden <= 1);
|
|
393
|
+
if (hidden <= 1) {
|
|
394
|
+
strip.scrollLeft = savedScrollLeft;
|
|
395
|
+
updateEdgeFades(strip);
|
|
374
396
|
return;
|
|
375
|
-
let total = 0;
|
|
376
|
-
let cutoff = buttons.length;
|
|
377
|
-
for (let i = 0; i < buttons.length; i++) {
|
|
378
|
-
total += buttons[i].offsetWidth;
|
|
379
|
-
if (total > available) {
|
|
380
|
-
cutoff = i;
|
|
381
|
-
break;
|
|
382
|
-
}
|
|
383
397
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
for (let i = 0; i < buttons.length; i++) {
|
|
389
|
-
total += buttons[i].offsetWidth;
|
|
390
|
-
if (total > available - OVERFLOW_TRIGGER_WIDTH) {
|
|
391
|
-
cutoff = i;
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
398
|
+
renderOverflowMenu(el, container, getChildTabs(el));
|
|
399
|
+
const triggerCost = container.clientWidth - strip.clientWidth;
|
|
400
|
+
if (hidden <= triggerCost) {
|
|
401
|
+
removeOverflowMenu(shadowRoot);
|
|
394
402
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
403
|
+
strip.scrollLeft = savedScrollLeft;
|
|
404
|
+
updateEdgeFades(strip);
|
|
405
|
+
}
|
|
406
|
+
function updateEdgeFades(strip) {
|
|
407
|
+
const maxScroll = strip.scrollWidth - strip.clientWidth;
|
|
408
|
+
const fade = (distance) => `${distance > 1 ? Math.min(28, distance) : 0}px`;
|
|
409
|
+
strip.style.setProperty('--fade-left', fade(strip.scrollLeft));
|
|
410
|
+
strip.style.setProperty('--fade-right', fade(maxScroll - strip.scrollLeft));
|
|
411
|
+
}
|
|
412
|
+
function scrollActiveIntoView(el, activeId, smooth) {
|
|
413
|
+
const shadowRoot = el.shadowRoot;
|
|
414
|
+
if (!shadowRoot)
|
|
415
|
+
return;
|
|
416
|
+
const strip = shadowRoot.querySelector('.tab-strip');
|
|
417
|
+
const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
|
|
418
|
+
if (!strip || !button || strip.clientWidth <= 0)
|
|
419
|
+
return;
|
|
420
|
+
const centered = button.offsetLeft + button.offsetWidth / 2 - strip.clientWidth / 2;
|
|
421
|
+
const target = Math.max(0, Math.min(centered, strip.scrollWidth - strip.clientWidth));
|
|
422
|
+
if (Math.round(target) !== Math.round(strip.scrollLeft)) {
|
|
423
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
424
|
+
strip.scrollTo({ left: target, behavior: smooth && !reduce ? 'smooth' : 'auto' });
|
|
404
425
|
}
|
|
405
|
-
buttons.forEach((b) => b.classList.toggle('overflow-hidden', hidden.has(b)));
|
|
406
|
-
renderOverflowMenu(el, container, buttons.filter((b) => hidden.has(b)), tabs);
|
|
407
426
|
}
|
|
408
427
|
function render(el) {
|
|
409
428
|
const shadowRoot = el.shadowRoot;
|
|
410
429
|
if (!shadowRoot)
|
|
411
430
|
return;
|
|
412
|
-
const { width, height, placement } = getTabsAttributes(el);
|
|
431
|
+
const { width, height, placement, fixed } = getTabsAttributes(el);
|
|
413
432
|
const tabs = getChildTabs(el);
|
|
414
433
|
const activeId = getActiveTabId(el, tabs);
|
|
415
434
|
const activeIndex = activeId ? (findTabIndex(tabs, activeId) ?? 0) : 0;
|
|
@@ -423,27 +442,26 @@ function render(el) {
|
|
|
423
442
|
});
|
|
424
443
|
const existingContainer = shadowRoot.querySelector('.tabs-container');
|
|
425
444
|
const existingButtons = shadowRoot.querySelector('.tab-buttons');
|
|
445
|
+
const existingStrip = shadowRoot.querySelector('.tab-strip');
|
|
426
446
|
const existingViewport = shadowRoot.querySelector('.panels-viewport');
|
|
427
447
|
ensureStyles(shadowRoot, { css: tabsStyles, id: 'ty-tabs' });
|
|
428
|
-
if (tabs.length > 7) {
|
|
429
|
-
console.warn(`[ty-tabs] More than 7 tabs detected (${tabs.length} tabs). ` +
|
|
430
|
-
'This may cause overflow and poor UX. ' +
|
|
431
|
-
'Consider using sidebar navigation, accordion menu, or other patterns.');
|
|
432
|
-
}
|
|
433
448
|
el.style.setProperty('--tabs-width', width.includes('%') ? '100%' : width);
|
|
434
449
|
el.style.setProperty('--tabs-height', height);
|
|
435
450
|
el.style.setProperty('--active-index', String(activeIndex));
|
|
436
|
-
if (existingContainer && existingButtons && existingViewport) {
|
|
451
|
+
if (existingContainer && existingButtons && existingStrip && existingViewport) {
|
|
437
452
|
existingContainer.setAttribute('data-placement', placement);
|
|
453
|
+
existingContainer.toggleAttribute('data-fixed', fixed);
|
|
454
|
+
const savedScrollLeft = existingStrip.scrollLeft;
|
|
438
455
|
existingButtons.querySelector('.tab-overflow-trigger')?.remove();
|
|
439
|
-
const allButtons =
|
|
456
|
+
const allButtons = existingStrip.querySelectorAll('.tab-button');
|
|
440
457
|
allButtons.forEach(button => button.remove());
|
|
441
458
|
const buttonsHtml = renderButtonsOnly(el, tabs, activeId);
|
|
442
459
|
const tempDiv = document.createElement('div');
|
|
443
460
|
tempDiv.innerHTML = buttonsHtml;
|
|
444
461
|
Array.from(tempDiv.children).forEach(button => {
|
|
445
|
-
|
|
462
|
+
existingStrip.appendChild(button);
|
|
446
463
|
});
|
|
464
|
+
existingStrip.scrollLeft = savedScrollLeft;
|
|
447
465
|
setupEventListeners(el, shadowRoot, tabs);
|
|
448
466
|
updateAriaAttributes(el, shadowRoot, activeId || '');
|
|
449
467
|
requestAnimationFrame(() => {
|
|
@@ -453,10 +471,11 @@ function render(el) {
|
|
|
453
471
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
454
472
|
}
|
|
455
473
|
updateTransform(el, activeIndex);
|
|
474
|
+
updateOverflow(el);
|
|
456
475
|
if (activeId) {
|
|
457
476
|
updateMarker(el, activeId);
|
|
477
|
+
scrollActiveIntoView(el, activeId, true);
|
|
458
478
|
}
|
|
459
|
-
updateOverflow(el);
|
|
460
479
|
});
|
|
461
480
|
if (activeId) {
|
|
462
481
|
updatePanelInteraction(el, activeId);
|
|
@@ -464,7 +483,7 @@ function render(el) {
|
|
|
464
483
|
}
|
|
465
484
|
else {
|
|
466
485
|
shadowRoot.innerHTML = `
|
|
467
|
-
<div class="tabs-container" data-placement="${placement}">
|
|
486
|
+
<div class="tabs-container" data-placement="${placement}"${fixed ? ' data-fixed' : ''}>
|
|
468
487
|
${renderTabButtons(el, tabs, activeId)}
|
|
469
488
|
<div class="panels-viewport" part="panels-container">
|
|
470
489
|
<div class="panels-wrapper">
|
|
@@ -480,14 +499,17 @@ function render(el) {
|
|
|
480
499
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
481
500
|
}
|
|
482
501
|
updateTransform(el, activeIndex);
|
|
502
|
+
updateOverflow(el);
|
|
483
503
|
if (activeId) {
|
|
484
504
|
updateMarker(el, activeId);
|
|
505
|
+
scrollActiveIntoView(el, activeId, false);
|
|
485
506
|
}
|
|
486
|
-
updateOverflow(el);
|
|
487
507
|
});
|
|
488
508
|
setupEventListeners(el, shadowRoot, tabs);
|
|
489
509
|
updateAriaAttributes(el, shadowRoot, activeId || '');
|
|
490
510
|
setupResizeObserver(el);
|
|
511
|
+
const strip = shadowRoot.querySelector('.tab-strip');
|
|
512
|
+
strip?.addEventListener('scroll', () => updateEdgeFades(strip), { passive: true });
|
|
491
513
|
if (activeId) {
|
|
492
514
|
updatePanelInteraction(el, activeId);
|
|
493
515
|
}
|
|
@@ -511,7 +533,7 @@ function cleanup(el) {
|
|
|
511
533
|
}
|
|
512
534
|
export class TyTabs extends HTMLElement {
|
|
513
535
|
static get observedAttributes() {
|
|
514
|
-
return ['width', 'height', 'active', 'placement'];
|
|
536
|
+
return ['width', 'height', 'active', 'placement', 'fixed'];
|
|
515
537
|
}
|
|
516
538
|
constructor() {
|
|
517
539
|
super();
|
|
@@ -540,10 +562,14 @@ export class TyTabs extends HTMLElement {
|
|
|
540
562
|
}
|
|
541
563
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
542
564
|
if (name === 'active') {
|
|
543
|
-
if (newValue)
|
|
544
|
-
|
|
565
|
+
if (!newValue)
|
|
566
|
+
return;
|
|
567
|
+
if (!this.shadowRoot?.querySelector(`[data-tab-id='${newValue}']`)) {
|
|
568
|
+
render(this);
|
|
569
|
+
return;
|
|
545
570
|
}
|
|
546
|
-
|
|
571
|
+
updateActiveTabState(this, newValue, oldValue);
|
|
572
|
+
scrollActiveIntoView(this, newValue, true);
|
|
547
573
|
}
|
|
548
574
|
else {
|
|
549
575
|
render(this);
|
|
@@ -44,6 +44,15 @@ export const selectBaseStyles = `
|
|
|
44
44
|
width: auto;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/* A custom slot="trigger" is the same situation as compact: the trigger is
|
|
48
|
+
the consumer's own button, not a form field — hug it so chips/toolbar
|
|
49
|
+
items can sit right next to it. [has-trigger] is reflected by
|
|
50
|
+
setupTriggerSlot() because Blink does not parse :has() inside :host(). */
|
|
51
|
+
:host([has-trigger]) {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
width: auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
.dropdown-dialog {
|
|
48
57
|
position: fixed;
|
|
49
58
|
width: var(--dropdown-width, 200px);
|
package/lib/styles/tab.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tab Component Styles
|
|
3
3
|
*/
|
|
4
|
-
export declare const tabStyles = "\n:host {\n width: var(--tabs-width, 100%);\n height: calc(var(--tabs-height, 400px) - var(--buttons-height, 48px));\n display: block;\n box-sizing: border-box;\n flex-shrink: 0;\n transition: opacity var(--transition-duration, 400ms) var(--transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0ms !important;\n }\n}\n\n.tab-wrapper {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.tab-panel {\n width: 100%;\n height: 100%;\n overflow: auto;\n box-sizing: border-box;\n}\n\n.tab-panel.ty-custom-scroll {\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n.tab-panel.ty-custom-scroll::-webkit-scrollbar {\n display: none;\n}\n\n.tab-wrapper:hover .ty-scrollbar-track-y.has-overflow {\n opacity: 1;\n}\n\n\n/* Vertical track */\n.ty-scrollbar-track-y {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-y.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-y.has-overflow:hover,\n.ty-scrollbar-track-y.has-overflow.scrolling,\n.ty-scrollbar-track-y.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-y::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-y:hover::before,\n.ty-scrollbar-track-y.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-y {\n position: absolute;\n right: 0;\n width: 100%;\n min-height: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-y:hover,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-y:active,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Horizontal track */\n.ty-scrollbar-track-x {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-x.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-x.has-overflow:hover,\n.ty-scrollbar-track-x.has-overflow.scrolling,\n.ty-scrollbar-track-x.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-x::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-x:hover::before,\n.ty-scrollbar-track-x.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-x {\n position: absolute;\n bottom: 0;\n height: 100%;\n min-width: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-x:hover,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-x:active,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Touch devices: hide the custom scrollbar */\n@media (pointer: coarse) and (hover: none) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x {\n display: none !important;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x,\n .ty-scrollbar-thumb-y,\n .ty-scrollbar-thumb-x {\n transition-duration: 0ms !important;\n }\n}\n\n";
|
|
4
|
+
export declare const tabStyles = "\n:host {\n width: var(--tabs-width, 100%);\n height: calc(var(--tabs-height, 400px) - var(--buttons-height, 48px));\n display: block;\n box-sizing: border-box;\n flex-shrink: 0;\n transition: opacity var(--ty-tabs-transition-duration, 400ms) var(--ty-tabs-transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0ms !important;\n }\n}\n\n.tab-wrapper {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.tab-panel {\n width: 100%;\n height: 100%;\n overflow: auto;\n box-sizing: border-box;\n}\n\n.tab-panel.ty-custom-scroll {\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n.tab-panel.ty-custom-scroll::-webkit-scrollbar {\n display: none;\n}\n\n.tab-wrapper:hover .ty-scrollbar-track-y.has-overflow {\n opacity: 1;\n}\n\n\n/* Vertical track */\n.ty-scrollbar-track-y {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-y.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-y.has-overflow:hover,\n.ty-scrollbar-track-y.has-overflow.scrolling,\n.ty-scrollbar-track-y.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-y::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-y:hover::before,\n.ty-scrollbar-track-y.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-y {\n position: absolute;\n right: 0;\n width: 100%;\n min-height: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-y:hover,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-y:active,\n.ty-scrollbar-track-y.dragging .ty-scrollbar-thumb-y {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Horizontal track */\n.ty-scrollbar-track-x {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: var(--ty-scrollbar-width, 8px);\n z-index: 3;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease-out;\n cursor: pointer;\n}\n\n.ty-scrollbar-track-x.has-overflow {\n pointer-events: auto;\n}\n\n.ty-scrollbar-track-x.has-overflow:hover,\n.ty-scrollbar-track-x.has-overflow.scrolling,\n.ty-scrollbar-track-x.dragging {\n opacity: 1;\n}\n\n.ty-scrollbar-track-x::before {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--ty-scrollbar-track, transparent);\n border-radius: var(--ty-scrollbar-radius, 4px);\n opacity: 0;\n transition: opacity 0.15s ease-out;\n}\n\n.ty-scrollbar-track-x:hover::before,\n.ty-scrollbar-track-x.dragging::before {\n opacity: 1;\n background: var(--ty-scrollbar-track-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 8%, transparent));\n}\n\n.ty-scrollbar-thumb-x {\n position: absolute;\n bottom: 0;\n height: 100%;\n min-width: var(--ty-scrollbar-thumb-min-height, 30px);\n background: var(--ty-scrollbar-thumb, color-mix(in oklab, var(--ty-color-neutral-bold) 40%, transparent));\n border-radius: var(--ty-scrollbar-radius, 4px);\n transition: var(--ty-local-transition, background 0.15s ease-out);\n box-sizing: border-box;\n border: 1px solid transparent;\n}\n\n.ty-scrollbar-thumb-x:hover,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-hover, color-mix(in oklab, var(--ty-color-neutral-bold) 55%, transparent));\n}\n\n.ty-scrollbar-thumb-x:active,\n.ty-scrollbar-track-x.dragging .ty-scrollbar-thumb-x {\n background: var(--ty-scrollbar-thumb-active, color-mix(in oklab, var(--ty-color-neutral-bold) 70%, transparent));\n}\n\n/* Touch devices: hide the custom scrollbar */\n@media (pointer: coarse) and (hover: none) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x {\n display: none !important;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ty-scrollbar-track-y,\n .ty-scrollbar-track-x,\n .ty-scrollbar-thumb-y,\n .ty-scrollbar-thumb-x {\n transition-duration: 0ms !important;\n }\n}\n\n";
|
package/lib/styles/tab.js
CHANGED
|
@@ -6,7 +6,7 @@ export const tabStyles = `
|
|
|
6
6
|
display: block;
|
|
7
7
|
box-sizing: border-box;
|
|
8
8
|
flex-shrink: 0;
|
|
9
|
-
transition: opacity var(--transition-duration, 400ms) var(--transition-easing, ease-in-out);
|
|
9
|
+
transition: opacity var(--ty-tabs-transition-duration, 400ms) var(--ty-tabs-transition-easing, ease-in-out);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
@media (prefers-reduced-motion: reduce) {
|
package/lib/styles/tabs.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Tabs Component Styles — ty-tabs container: top/bottom placement, animated
|
|
3
3
|
* active marker, carousel viewport. Customizable via CSS Parts (::part).
|
|
4
4
|
*/
|
|
5
|
-
export declare const tabsStyles = "\n:host {\n display: block;\n width: var(--tabs-width, 100%);\n height: var(--tabs-height, 400px);\n box-sizing: border-box;\n}\n\n.tabs-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n}\n\n.tabs-container[data-placement=\"bottom\"] {\n flex-direction: column-reverse;\n}\n\n/* Tab buttons container \u2014 exposed as a CSS Part for full styling control */\n.tab-buttons {\n display: flex;\n gap: 0;\n flex-shrink: 0;\n position: relative;\n
|
|
5
|
+
export declare const tabsStyles = "\n:host {\n display: block;\n width: var(--tabs-width, 100%);\n height: var(--tabs-height, 400px);\n box-sizing: border-box;\n}\n\n.tabs-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n}\n\n.tabs-container[data-placement=\"bottom\"] {\n flex-direction: column-reverse;\n}\n\n/* Tab buttons container \u2014 exposed as a CSS Part for full styling control */\n.tab-buttons {\n display: flex;\n gap: 0;\n flex-shrink: 0;\n position: relative;\n\n /* Default minimal styling - customize via ::part(buttons-container) */\n background: transparent;\n}\n\n/* Separator drawn as a pseudo-element, not a border: a border sits OUTSIDE the\n strip's box, so the marker (absolutely positioned inside the strip) could only\n ever stop just above it, reading as two stacked lines. As an overlaid line it\n lands inside the marker's range, and .tab-strip's z-index lets the marker\n cover it. */\n.tab-buttons::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 1px;\n /* --ty-tabs-separator: transparent removes the line (e.g. with a pill marker) */\n background: var(--ty-tabs-separator, var(--ty-border));\n pointer-events: none;\n}\n\n.tabs-container[data-placement=\"bottom\"] .tab-buttons::after {\n bottom: auto;\n top: 0;\n}\n\n/* Scrollable strip holding the buttons + marker. Tabs never collapse \u2014 the\n strip scrolls (scrollbar hidden; touch/wheel still work) and activation\n glides the active tab into view. The \"\u2026\" trigger sits OUTSIDE this strip,\n pinned at the edge. position:relative makes it the offsetParent for the\n marker, so the marker scrolls with the buttons and can't desync. */\n.tab-strip {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n position: relative;\n /* Paints the marker + buttons over .tab-buttons::after (the separator line) */\n z-index: 1;\n overflow-x: auto;\n scrollbar-width: none;\n /* Edge fades \u2014 a mask, not overlays, so it works over any background.\n --fade-left/right are set from the scroll listener: 0px at a hard edge,\n 28px when more tabs continue past that side. */\n --fade-left: 0px;\n --fade-right: 0px;\n -webkit-mask-image: linear-gradient(to right,\n transparent, black var(--fade-left),\n black calc(100% - var(--fade-right)), transparent);\n mask-image: linear-gradient(to right,\n transparent, black var(--fade-left),\n black calc(100% - var(--fade-right)), transparent);\n}\n\n.tab-strip::-webkit-scrollbar {\n display: none;\n}\n\n/* When every tab fits there is nothing to scroll, so drop the clipping \u2014\n both overflow and the mask cut a custom marker's box-shadow off at the\n strip edge. Set by updateOverflow(); clipping stays the pre-measurement\n default so overflowing tabs can't spill for a frame on first render. */\n.tab-strip.unclipped {\n overflow: visible;\n -webkit-mask-image: none;\n mask-image: none;\n}\n\n/* Marker wrapper \u2014 exposed as a CSS Part for custom marker styling */\n.marker-wrapper {\n position: absolute;\n z-index: 0;\n /* Behind buttons */\n pointer-events: none;\n /* Don't block clicks */\n transition: left var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),\n width var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),\n height var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out),\n top var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out);\n}\n\n.default-marker {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 3px;\n width: 100%;\n background: var(--ty-color-primary);\n pointer-events: none;\n}\n\n/* Bottom placement: marker rides the top edge, next to the content it marks */\n.tabs-container[data-placement=\"bottom\"] .default-marker {\n bottom: auto;\n top: 0;\n}\n\n::slotted([slot=\"marker\"]) {\n display: block;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n}\n\n.tab-button {\n /* Material's floor for SCROLLABLE tabs. 120px is the fixed-tab number and it\n manufactured overflow: five short labels claimed 600px whether they needed\n it or not. Equal-width rhythm is what the \"fixed\" attribute is for. */\n min-width: var(--ty-tab-min-width, 72px);\n /* Scrollable tabs keep their natural width and let the strip scroll \u2014 that is\n the whole contract. Without this they are flex items with the default\n shrink of 1, so they squeeze toward the floor and their labels ellipsize\n before the strip ever overflows. Squeezing is what \"fixed\" is for. */\n flex-shrink: 0;\n padding: 6px 12px;\n border: none;\n background: transparent;\n cursor: pointer;\n font: inherit;\n color: var(--ty-text-soft);\n transition: var(--ty-local-transition, color 200ms, background-color 200ms);\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n font-weight: var(--ty-font-bold);\n font-size: var(--ty-font-sm);\n position: relative;\n /* Establish stacking context */\n z-index: 10;\n /* Above marker */\n}\n\n/* Fixed tabs: the bar is divided between them instead of scrolling. Material\n calls this \"fixed tabs\" \u2014 for a small, known set of short labels. No\n min-width floor, no overflow, no \"\u2026\". */\n.tabs-container[data-fixed] .tab-button {\n flex: 1 1 0;\n min-width: 0;\n}\n\n/* Fixed tabs only. A scrollable tab is never narrower than its label, so\n ellipsizing there would hide text the user could simply scroll to. */\n.tabs-container[data-fixed] .tab-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tab-button[aria-selected=true] {\n color: var(--ty-text-strong);\n}\n\n.tab-button[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.tab-button:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n/* Overflow \"more\" trigger (pill) + menu */\n.tab-overflow-trigger {\n flex-shrink: 0;\n align-self: center;\n min-width: 36px;\n height: 24px;\n margin: 0 8px;\n padding: 0 10px;\n border: 1px solid var(--ty-border);\n border-radius: 9999px;\n background: transparent;\n cursor: pointer;\n color: var(--ty-text-soft);\n font-size: 16px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n transition: color 200ms, border-color 200ms;\n}\n\n.tab-overflow-trigger:hover {\n color: var(--ty-text-strong);\n border-color: var(--ty-text-soft);\n}\n\n.tab-overflow-trigger:focus-visible {\n outline: 2px solid var(--ty-color-primary);\n outline-offset: -2px;\n}\n\n.tab-overflow-menu {\n display: flex;\n flex-direction: column;\n min-width: 160px;\n max-height: 300px;\n overflow-y: auto;\n padding: var(--ty-spacing-2, 4px);\n background: var(--ty-surface-floating);\n border-radius: var(--ty-radius-md);\n box-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n}\n\n.tab-overflow-item {\n padding: var(--ty-spacing-2, 6px) var(--ty-spacing-3, 12px);\n border: none;\n background: transparent;\n text-align: left;\n cursor: pointer;\n border-radius: var(--ty-radius-md);\n color: var(--ty-text);\n font: inherit;\n font-size: var(--ty-font-sm);\n white-space: nowrap;\n}\n\n.tab-overflow-item:hover {\n background: var(--ty-bg-neutral-soft);\n color: var(--ty-text-strong);\n}\n\n.tab-overflow-item[data-active=\"true\"] {\n color: var(--ty-color-primary);\n font-weight: var(--ty-font-bold);\n}\n\n.tab-overflow-item[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* Panels viewport \u2014 exposed as a CSS Part */\n.panels-viewport {\n position: relative;\n flex: 1;\n overflow: hidden;\n /* Critical: hides off-screen panels */\n min-height: 0;\n /* Allows flex child to shrink */\n}\n\n/* Panels wrapper slides horizontally */\n.panels-wrapper {\n display: flex;\n height: 100%;\n transition: transform var(--ty-tabs-transition-duration, 300ms) var(--ty-tabs-transition-easing, ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panels-wrapper {\n transition-duration: 0ms !important;\n }\n\n .marker-wrapper {\n transition-duration: 0ms !important;\n }\n}\n\n::slotted(ty-tab) {\n /* Carousel page width. --tabs-page-width is the measured pixel width, set by\n the ResizeObserver for percentage-width tabs so every panel is exactly one\n page wide. It is deliberately NOT --tabs-width: that one sizes :host, and\n feeding a measured pixel value back into the host's own width is a\n self-referential loop (see setupResizeObserver). */\n width: var(--tabs-page-width, var(--tabs-width, 100%));\n height: 100%;\n flex-shrink: 0;\n}\n";
|