tyrell-components 1.0.0-RC13 → 1.0.0-RC14
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/README.md +1 -1
- package/css/tyrell-theme.css +167 -178
- package/css/tyrell.css +19 -0
- package/dist/tyrell-theme.css +167 -178
- package/dist/tyrell.css +19 -0
- package/dist/tyrell.js +1 -1
- package/lib/base/ty-component.d.ts +20 -10
- package/lib/base/ty-component.js +21 -144
- package/lib/components/button.d.ts +2 -16
- package/lib/components/button.js +0 -36
- package/lib/components/calendar-month.d.ts +2 -48
- package/lib/components/calendar-month.js +4 -166
- package/lib/components/calendar-navigation.d.ts +3 -48
- package/lib/components/calendar-navigation.js +3 -126
- package/lib/components/calendar.d.ts +3 -80
- package/lib/components/calendar.js +8 -230
- package/lib/components/checkbox.d.ts +4 -71
- package/lib/components/checkbox.js +7 -151
- package/lib/components/copy.d.ts +3 -56
- package/lib/components/copy.js +1 -120
- package/lib/components/date-picker.d.ts +13 -162
- package/lib/components/date-picker.js +41 -592
- package/lib/components/file-upload.d.ts +9 -15
- package/lib/components/file-upload.js +19 -41
- package/lib/components/icon.d.ts +3 -24
- package/lib/components/icon.js +1 -90
- package/lib/components/input.d.ts +7 -107
- package/lib/components/input.js +5 -193
- package/lib/components/modal.d.ts +17 -26
- package/lib/components/modal.js +57 -201
- package/lib/components/option.d.ts +2 -9
- package/lib/components/option.js +0 -48
- package/lib/components/popup.d.ts +2 -17
- package/lib/components/popup.js +8 -139
- package/lib/components/radio.d.ts +3 -23
- package/lib/components/radio.js +10 -65
- package/lib/components/resize-observer.d.ts +3 -30
- package/lib/components/resize-observer.js +12 -46
- package/lib/components/scroll-container.d.ts +2 -12
- package/lib/components/scroll-container.js +3 -46
- package/lib/components/select.d.ts +22 -153
- package/lib/components/select.js +33 -590
- package/lib/components/selected-tags.d.ts +4 -47
- package/lib/components/selected-tags.js +1 -95
- package/lib/components/step.d.ts +1 -6
- package/lib/components/step.js +0 -6
- package/lib/components/switch.d.ts +0 -11
- package/lib/components/switch.js +2 -36
- package/lib/components/tab.d.ts +1 -6
- package/lib/components/tab.js +0 -7
- package/lib/components/tabs.d.ts +5 -63
- package/lib/components/tabs.js +20 -283
- package/lib/components/tag.d.ts +2 -36
- package/lib/components/tag.js +5 -105
- package/lib/components/textarea.d.ts +6 -86
- package/lib/components/textarea.js +2 -173
- package/lib/components/tooltip.d.ts +3 -11
- package/lib/components/tooltip.js +24 -160
- package/lib/components/wizard.d.ts +6 -54
- package/lib/components/wizard.js +15 -228
- package/lib/icons/fontawesome/brands.d.ts +0 -1
- package/lib/icons/fontawesome/brands.js +0 -8
- package/lib/icons/fontawesome/regular.d.ts +0 -1
- package/lib/icons/fontawesome/regular.js +0 -8
- package/lib/icons/fontawesome/solid.d.ts +0 -1
- package/lib/icons/fontawesome/solid.js +0 -8
- package/lib/icons/heroicons/micro.d.ts +0 -1
- package/lib/icons/heroicons/micro.js +0 -8
- package/lib/icons/heroicons/mini.d.ts +0 -1
- package/lib/icons/heroicons/mini.js +0 -8
- package/lib/icons/heroicons/outline.d.ts +0 -1
- package/lib/icons/heroicons/outline.js +0 -8
- package/lib/icons/heroicons/solid.d.ts +0 -1
- package/lib/icons/heroicons/solid.js +0 -8
- package/lib/icons/lucide.d.ts +0 -1
- package/lib/icons/lucide.js +0 -1872
- package/lib/icons/material/filled.d.ts +0 -1
- package/lib/icons/material/filled.js +0 -10
- package/lib/icons/material/outlined.d.ts +0 -1
- package/lib/icons/material/outlined.js +0 -10
- package/lib/icons/material/round.d.ts +0 -1
- package/lib/icons/material/round.js +0 -10
- package/lib/icons/material/sharp.d.ts +0 -1
- package/lib/icons/material/sharp.js +0 -10
- package/lib/icons/material/two-tone.d.ts +0 -1
- package/lib/icons/material/two-tone.js +0 -10
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -20
- package/lib/styles/button.d.ts +2 -20
- package/lib/styles/button.js +22 -122
- package/lib/styles/calendar-month.d.ts +1 -5
- package/lib/styles/calendar-month.js +4 -61
- package/lib/styles/calendar-navigation.d.ts +2 -6
- package/lib/styles/calendar-navigation.js +2 -27
- package/lib/styles/calendar.d.ts +1 -2
- package/lib/styles/calendar.js +1 -14
- package/lib/styles/checkbox.d.ts +0 -1
- package/lib/styles/checkbox.js +1 -23
- package/lib/styles/copy.d.ts +0 -1
- package/lib/styles/copy.js +2 -17
- package/lib/styles/custom-scrollbar.d.ts +1 -2
- package/lib/styles/custom-scrollbar.js +3 -18
- package/lib/styles/date-picker.d.ts +1 -5
- package/lib/styles/date-picker.js +35 -39
- package/lib/styles/file-upload.d.ts +1 -2
- package/lib/styles/file-upload.js +1 -16
- package/lib/styles/icon.d.ts +2 -6
- package/lib/styles/icon.js +7 -21
- package/lib/styles/input.d.ts +1 -6
- package/lib/styles/input.js +5 -54
- package/lib/styles/modal.d.ts +1 -2
- package/lib/styles/modal.js +21 -23
- package/lib/styles/option.d.ts +2 -6
- package/lib/styles/option.js +17 -12
- package/lib/styles/popup.d.ts +1 -2
- package/lib/styles/popup.js +26 -30
- package/lib/styles/radio.d.ts +0 -1
- package/lib/styles/radio.js +2 -16
- package/lib/styles/resize-observer.d.ts +1 -2
- package/lib/styles/resize-observer.js +0 -7
- package/lib/styles/scroll-container.d.ts +1 -2
- package/lib/styles/scroll-container.js +0 -19
- package/lib/styles/select-base.d.ts +0 -1
- package/lib/styles/select-base.js +68 -79
- package/lib/styles/select.d.ts +1 -2
- package/lib/styles/select.js +7 -15
- package/lib/styles/step.d.ts +1 -2
- package/lib/styles/step.js +0 -4
- package/lib/styles/switch.d.ts +0 -1
- package/lib/styles/switch.js +2 -20
- package/lib/styles/tab.d.ts +1 -2
- package/lib/styles/tab.js +0 -5
- package/lib/styles/tabs.d.ts +3 -11
- package/lib/styles/tabs.js +10 -48
- package/lib/styles/tag.d.ts +1 -5
- package/lib/styles/tag.js +2 -31
- package/lib/styles/textarea.d.ts +1 -2
- package/lib/styles/textarea.js +2 -45
- package/lib/styles/tooltip.d.ts +0 -1
- package/lib/styles/tooltip.js +0 -10
- package/lib/styles/wizard.d.ts +5 -21
- package/lib/styles/wizard.js +8 -88
- package/lib/types/common.d.ts +0 -2
- package/lib/types/common.js +0 -12
- package/lib/utils/calendar-utils.d.ts +4 -21
- package/lib/utils/calendar-utils.js +4 -177
- package/lib/utils/custom-scrollbar.d.ts +6 -20
- package/lib/utils/custom-scrollbar.js +1 -45
- package/lib/utils/flavor-sheet.d.ts +0 -1
- package/lib/utils/flavor-sheet.js +0 -23
- package/lib/utils/icon-registry.d.ts +0 -2
- package/lib/utils/icon-registry.js +1 -107
- package/lib/utils/icons.d.ts +0 -1
- package/lib/utils/icons.js +0 -9
- package/lib/utils/loader-registry.d.ts +0 -1
- package/lib/utils/loader-registry.js +0 -32
- package/lib/utils/locale.d.ts +2 -23
- package/lib/utils/locale.js +0 -86
- package/lib/utils/mobile.d.ts +0 -1
- package/lib/utils/mobile.js +0 -17
- package/lib/utils/number-format.d.ts +6 -3
- package/lib/utils/number-format.js +11 -67
- package/lib/utils/positioning.d.ts +50 -7
- package/lib/utils/positioning.js +52 -62
- package/lib/utils/property-capture.d.ts +5 -27
- package/lib/utils/property-capture.js +0 -108
- package/lib/utils/property-manager.d.ts +0 -1
- package/lib/utils/property-manager.js +0 -70
- package/lib/utils/resize-observer.d.ts +0 -1
- package/lib/utils/resize-observer.js +0 -36
- package/lib/utils/scroll-lock.d.ts +4 -11
- package/lib/utils/scroll-lock.js +0 -107
- package/lib/utils/styles.d.ts +0 -1
- package/lib/utils/styles.js +0 -24
- package/lib/version.d.ts +1 -2
- package/lib/version.js +1 -11
- package/package.json +2 -2
- package/lib/base/ty-component.d.ts.map +0 -1
- package/lib/base/ty-component.js.map +0 -1
- package/lib/components/button.d.ts.map +0 -1
- package/lib/components/button.js.map +0 -1
- package/lib/components/calendar-month.d.ts.map +0 -1
- package/lib/components/calendar-month.js.map +0 -1
- package/lib/components/calendar-navigation.d.ts.map +0 -1
- package/lib/components/calendar-navigation.js.map +0 -1
- package/lib/components/calendar.d.ts.map +0 -1
- package/lib/components/calendar.js.map +0 -1
- package/lib/components/checkbox.d.ts.map +0 -1
- package/lib/components/checkbox.js.map +0 -1
- package/lib/components/copy.d.ts.map +0 -1
- package/lib/components/copy.js.map +0 -1
- package/lib/components/date-picker.d.ts.map +0 -1
- package/lib/components/date-picker.js.map +0 -1
- package/lib/components/file-upload.d.ts.map +0 -1
- package/lib/components/file-upload.js.map +0 -1
- package/lib/components/icon.d.ts.map +0 -1
- package/lib/components/icon.js.map +0 -1
- package/lib/components/input.d.ts.map +0 -1
- package/lib/components/input.js.map +0 -1
- package/lib/components/modal.d.ts.map +0 -1
- package/lib/components/modal.js.map +0 -1
- package/lib/components/option.d.ts.map +0 -1
- package/lib/components/option.js.map +0 -1
- package/lib/components/popup.d.ts.map +0 -1
- package/lib/components/popup.js.map +0 -1
- package/lib/components/radio.d.ts.map +0 -1
- package/lib/components/radio.js.map +0 -1
- package/lib/components/resize-observer.d.ts.map +0 -1
- package/lib/components/resize-observer.js.map +0 -1
- package/lib/components/scroll-container.d.ts.map +0 -1
- package/lib/components/scroll-container.js.map +0 -1
- package/lib/components/select.d.ts.map +0 -1
- package/lib/components/select.js.map +0 -1
- package/lib/components/selected-tags.d.ts.map +0 -1
- package/lib/components/selected-tags.js.map +0 -1
- package/lib/components/step.d.ts.map +0 -1
- package/lib/components/step.js.map +0 -1
- package/lib/components/switch.d.ts.map +0 -1
- package/lib/components/switch.js.map +0 -1
- package/lib/components/tab.d.ts.map +0 -1
- package/lib/components/tab.js.map +0 -1
- package/lib/components/tabs.d.ts.map +0 -1
- package/lib/components/tabs.js.map +0 -1
- package/lib/components/tag.d.ts.map +0 -1
- package/lib/components/tag.js.map +0 -1
- package/lib/components/textarea.d.ts.map +0 -1
- package/lib/components/textarea.js.map +0 -1
- package/lib/components/tooltip.d.ts.map +0 -1
- package/lib/components/tooltip.js.map +0 -1
- package/lib/components/wizard.d.ts.map +0 -1
- package/lib/components/wizard.js.map +0 -1
- package/lib/icons/fontawesome/brands.d.ts.map +0 -1
- package/lib/icons/fontawesome/brands.js.map +0 -1
- package/lib/icons/fontawesome/regular.d.ts.map +0 -1
- package/lib/icons/fontawesome/regular.js.map +0 -1
- package/lib/icons/fontawesome/solid.d.ts.map +0 -1
- package/lib/icons/fontawesome/solid.js.map +0 -1
- package/lib/icons/heroicons/micro.d.ts.map +0 -1
- package/lib/icons/heroicons/micro.js.map +0 -1
- package/lib/icons/heroicons/mini.d.ts.map +0 -1
- package/lib/icons/heroicons/mini.js.map +0 -1
- package/lib/icons/heroicons/outline.d.ts.map +0 -1
- package/lib/icons/heroicons/outline.js.map +0 -1
- package/lib/icons/heroicons/solid.d.ts.map +0 -1
- package/lib/icons/heroicons/solid.js.map +0 -1
- package/lib/icons/lucide.d.ts.map +0 -1
- package/lib/icons/lucide.js.map +0 -1
- package/lib/icons/material/filled.d.ts.map +0 -1
- package/lib/icons/material/filled.js.map +0 -1
- package/lib/icons/material/outlined.d.ts.map +0 -1
- package/lib/icons/material/outlined.js.map +0 -1
- package/lib/icons/material/round.d.ts.map +0 -1
- package/lib/icons/material/round.js.map +0 -1
- package/lib/icons/material/sharp.d.ts.map +0 -1
- package/lib/icons/material/sharp.js.map +0 -1
- package/lib/icons/material/two-tone.d.ts.map +0 -1
- package/lib/icons/material/two-tone.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/styles/button.d.ts.map +0 -1
- package/lib/styles/button.js.map +0 -1
- package/lib/styles/calendar-month.d.ts.map +0 -1
- package/lib/styles/calendar-month.js.map +0 -1
- package/lib/styles/calendar-navigation.d.ts.map +0 -1
- package/lib/styles/calendar-navigation.js.map +0 -1
- package/lib/styles/calendar.d.ts.map +0 -1
- package/lib/styles/calendar.js.map +0 -1
- package/lib/styles/checkbox.d.ts.map +0 -1
- package/lib/styles/checkbox.js.map +0 -1
- package/lib/styles/copy.d.ts.map +0 -1
- package/lib/styles/copy.js.map +0 -1
- package/lib/styles/custom-scrollbar.d.ts.map +0 -1
- package/lib/styles/custom-scrollbar.js.map +0 -1
- package/lib/styles/date-picker.d.ts.map +0 -1
- package/lib/styles/date-picker.js.map +0 -1
- package/lib/styles/file-upload.d.ts.map +0 -1
- package/lib/styles/file-upload.js.map +0 -1
- package/lib/styles/icon.d.ts.map +0 -1
- package/lib/styles/icon.js.map +0 -1
- package/lib/styles/input.d.ts.map +0 -1
- package/lib/styles/input.js.map +0 -1
- package/lib/styles/modal.d.ts.map +0 -1
- package/lib/styles/modal.js.map +0 -1
- package/lib/styles/multiselect.d.ts +0 -8
- package/lib/styles/multiselect.d.ts.map +0 -1
- package/lib/styles/multiselect.js +0 -849
- package/lib/styles/multiselect.js.map +0 -1
- package/lib/styles/option.d.ts.map +0 -1
- package/lib/styles/option.js.map +0 -1
- package/lib/styles/popup.d.ts.map +0 -1
- package/lib/styles/popup.js.map +0 -1
- package/lib/styles/radio.d.ts.map +0 -1
- package/lib/styles/radio.js.map +0 -1
- package/lib/styles/resize-observer.d.ts.map +0 -1
- package/lib/styles/resize-observer.js.map +0 -1
- package/lib/styles/scroll-container.d.ts.map +0 -1
- package/lib/styles/scroll-container.js.map +0 -1
- package/lib/styles/select-base.d.ts.map +0 -1
- package/lib/styles/select-base.js.map +0 -1
- package/lib/styles/select.d.ts.map +0 -1
- package/lib/styles/select.js.map +0 -1
- package/lib/styles/step.d.ts.map +0 -1
- package/lib/styles/step.js.map +0 -1
- package/lib/styles/switch.d.ts.map +0 -1
- package/lib/styles/switch.js.map +0 -1
- package/lib/styles/tab.d.ts.map +0 -1
- package/lib/styles/tab.js.map +0 -1
- package/lib/styles/tabs.d.ts.map +0 -1
- package/lib/styles/tabs.js.map +0 -1
- package/lib/styles/tag.d.ts.map +0 -1
- package/lib/styles/tag.js.map +0 -1
- package/lib/styles/textarea.d.ts.map +0 -1
- package/lib/styles/textarea.js.map +0 -1
- package/lib/styles/tooltip.d.ts.map +0 -1
- package/lib/styles/tooltip.js.map +0 -1
- package/lib/styles/wizard.d.ts.map +0 -1
- package/lib/styles/wizard.js.map +0 -1
- package/lib/types/common.d.ts.map +0 -1
- package/lib/types/common.js.map +0 -1
- package/lib/utils/calendar-utils.d.ts.map +0 -1
- package/lib/utils/calendar-utils.js.map +0 -1
- package/lib/utils/custom-scrollbar.d.ts.map +0 -1
- package/lib/utils/custom-scrollbar.js.map +0 -1
- package/lib/utils/flavor-sheet.d.ts.map +0 -1
- package/lib/utils/flavor-sheet.js.map +0 -1
- package/lib/utils/icon-registry.d.ts.map +0 -1
- package/lib/utils/icon-registry.js.map +0 -1
- package/lib/utils/icons.d.ts.map +0 -1
- package/lib/utils/icons.js.map +0 -1
- package/lib/utils/loader-registry.d.ts.map +0 -1
- package/lib/utils/loader-registry.js.map +0 -1
- package/lib/utils/locale.d.ts.map +0 -1
- package/lib/utils/locale.js.map +0 -1
- package/lib/utils/mobile.d.ts.map +0 -1
- package/lib/utils/mobile.js.map +0 -1
- package/lib/utils/number-format.d.ts.map +0 -1
- package/lib/utils/number-format.js.map +0 -1
- package/lib/utils/parse-boolean.d.ts +0 -39
- package/lib/utils/parse-boolean.d.ts.map +0 -1
- package/lib/utils/parse-boolean.js +0 -58
- package/lib/utils/parse-boolean.js.map +0 -1
- package/lib/utils/positioning.d.ts.map +0 -1
- package/lib/utils/positioning.js.map +0 -1
- package/lib/utils/property-capture.d.ts.map +0 -1
- package/lib/utils/property-capture.js.map +0 -1
- package/lib/utils/property-manager.d.ts.map +0 -1
- package/lib/utils/property-manager.js.map +0 -1
- package/lib/utils/resize-observer.d.ts.map +0 -1
- package/lib/utils/resize-observer.js.map +0 -1
- package/lib/utils/scroll-lock.d.ts.map +0 -1
- package/lib/utils/scroll-lock.js.map +0 -1
- package/lib/utils/styles.d.ts.map +0 -1
- package/lib/utils/styles.js.map +0 -1
- package/lib/version.d.ts.map +0 -1
- package/lib/version.js.map +0 -1
package/lib/components/tabs.js
CHANGED
|
@@ -1,79 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tabs Component
|
|
3
|
-
*
|
|
4
|
-
* A carousel-based tabs component with smooth animations and fixed container dimensions.
|
|
5
|
-
* Features horizontal sliding transitions, smart positioning marker, and independent panel scrolling.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Carousel animation with smooth sliding transitions
|
|
9
|
-
* - Fixed dimensions prevent layout shift between tabs
|
|
10
|
-
* - Animated marker that follows active tab button
|
|
11
|
-
* - Rich label support via slots (icons, badges, custom content)
|
|
12
|
-
* - Custom marker slot for complete active tab styling control
|
|
13
|
-
* - Independent panel scrolling with scroll position reset
|
|
14
|
-
* - ResizeObserver for responsive percentage widths
|
|
15
|
-
* - Smart rendering - only updates DOM when necessary
|
|
16
|
-
* - Accessibility with ARIA roles and attributes
|
|
17
|
-
* - Top/bottom placement for tab buttons
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* <!-- Basic tabs with text labels -->
|
|
21
|
-
* <ty-tabs width="800px" height="600px" active="general">
|
|
22
|
-
* <ty-tab id="general" label="General Settings">
|
|
23
|
-
* <div class="p-6">General content...</div>
|
|
24
|
-
* </ty-tab>
|
|
25
|
-
* <ty-tab id="advanced" label="Advanced Settings">
|
|
26
|
-
* <div class="p-6">Advanced content...</div>
|
|
27
|
-
* </ty-tab>
|
|
28
|
-
* </ty-tabs>
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* <!-- Rich labels with icons and badges -->
|
|
32
|
-
* <ty-tabs width="800px" height="600px">
|
|
33
|
-
* <!-- Rich label as direct child -->
|
|
34
|
-
* <span slot="label-profile" class="flex items-center gap-2">
|
|
35
|
-
* <ty-icon name="user" size="sm"></ty-icon>
|
|
36
|
-
* Profile
|
|
37
|
-
* </span>
|
|
38
|
-
*
|
|
39
|
-
* <span slot="label-notifications" class="flex items-center gap-2">
|
|
40
|
-
* <ty-icon name="bell" size="sm"></ty-icon>
|
|
41
|
-
* Notifications
|
|
42
|
-
* <span class="ty-bg-danger ty-text-danger++ px-2 py-0.5 rounded-full text-xs">5</span>
|
|
43
|
-
* </span>
|
|
44
|
-
*
|
|
45
|
-
* <!-- Tab panels -->
|
|
46
|
-
* <ty-tab id="profile">...</ty-tab>
|
|
47
|
-
* <ty-tab id="notifications">...</ty-tab>
|
|
48
|
-
* </ty-tabs>
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* <!-- Custom marker styling -->
|
|
52
|
-
* <ty-tabs width="100%" height="400px">
|
|
53
|
-
* <!-- Gradient marker slot -->
|
|
54
|
-
* <div slot="marker" style="
|
|
55
|
-
* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
56
|
-
* border-radius: 0.5rem;
|
|
57
|
-
* box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
58
|
-
* "></div>
|
|
59
|
-
*
|
|
60
|
-
* <ty-tab id="tab1" label="Dashboard">...</ty-tab>
|
|
61
|
-
* <ty-tab id="tab2" label="Analytics">...</ty-tab>
|
|
62
|
-
* </ty-tabs>
|
|
63
|
-
*/
|
|
64
1
|
import { ensureStyles } from '../utils/styles.js';
|
|
65
2
|
import { tabsStyles } from '../styles/tabs.js';
|
|
66
|
-
// ============================================================================
|
|
67
|
-
// WeakMaps for State Management
|
|
68
|
-
// ============================================================================
|
|
69
3
|
const eventHandlers = new WeakMap();
|
|
70
4
|
const resizeObservers = new WeakMap();
|
|
71
|
-
// ============================================================================
|
|
72
|
-
// Helper Functions - Attribute Parsing
|
|
73
|
-
// ============================================================================
|
|
74
|
-
/**
|
|
75
|
-
* Extract tabs configuration from element attributes
|
|
76
|
-
*/
|
|
77
5
|
function getTabsAttributes(el) {
|
|
78
6
|
return {
|
|
79
7
|
width: el.getAttribute('width') || '100%',
|
|
@@ -82,29 +10,15 @@ function getTabsAttributes(el) {
|
|
|
82
10
|
placement: (el.getAttribute('placement') || 'top'),
|
|
83
11
|
};
|
|
84
12
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Get all ty-tab child elements
|
|
87
|
-
*/
|
|
88
13
|
function getChildTabs(el) {
|
|
89
14
|
return Array.from(el.querySelectorAll('ty-tab'));
|
|
90
15
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Get ID from a ty-tab element
|
|
93
|
-
*/
|
|
94
16
|
function getTabId(tab) {
|
|
95
17
|
return tab.getAttribute('id');
|
|
96
18
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Check if ty-tabs has a direct child label slot for this tab-id.
|
|
99
|
-
* Looks in ty-tabs' light DOM for slot='label-{tab-id}' elements.
|
|
100
|
-
*/
|
|
101
19
|
function hasSlotLabel(tabsEl, tabId) {
|
|
102
20
|
return tabsEl.querySelector(`[slot='label-${tabId}']`) !== null;
|
|
103
21
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Determine label type: 'slot' (rich content) or 'text' (simple attribute).
|
|
106
|
-
* Checks ty-tabs element for slot='label-{tab-id}' direct children.
|
|
107
|
-
*/
|
|
108
22
|
function getTabLabelType(tabsEl, tab) {
|
|
109
23
|
const tabId = getTabId(tab);
|
|
110
24
|
if (tabId && hasSlotLabel(tabsEl, tabId)) {
|
|
@@ -112,51 +26,29 @@ function getTabLabelType(tabsEl, tab) {
|
|
|
112
26
|
}
|
|
113
27
|
return 'text';
|
|
114
28
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Check if tab is disabled
|
|
117
|
-
*/
|
|
118
29
|
function isTabDisabled(tab) {
|
|
119
30
|
return tab.hasAttribute('disabled');
|
|
120
31
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Check if user provided custom marker content via slot
|
|
123
|
-
*/
|
|
124
32
|
function hasCustomMarker(el) {
|
|
125
33
|
return el.querySelector('[slot="marker"]') !== null;
|
|
126
34
|
}
|
|
127
|
-
// ============================================================================
|
|
128
|
-
// Helper Functions - Active Tab Management
|
|
129
|
-
// ============================================================================
|
|
130
|
-
/**
|
|
131
|
-
* Find index of tab with given ID
|
|
132
|
-
*/
|
|
133
35
|
function findTabIndex(tabs, tabId) {
|
|
134
36
|
const index = tabs.findIndex(tab => getTabId(tab) === tabId);
|
|
135
37
|
return index >= 0 ? index : undefined;
|
|
136
38
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Get the active tab ID, defaulting to first tab if not specified
|
|
139
|
-
*/
|
|
140
39
|
function getActiveTabId(el, tabs) {
|
|
141
40
|
const activeAttr = el.getAttribute('active');
|
|
142
41
|
if (activeAttr && findTabIndex(tabs, activeAttr) !== undefined) {
|
|
143
42
|
return activeAttr;
|
|
144
43
|
}
|
|
145
|
-
// Default to first tab
|
|
146
44
|
if (tabs.length > 0) {
|
|
147
45
|
return getTabId(tabs[0]);
|
|
148
46
|
}
|
|
149
47
|
return null;
|
|
150
48
|
}
|
|
151
|
-
/**
|
|
152
|
-
* Set the active tab by ID
|
|
153
|
-
*/
|
|
154
49
|
function setActiveTab(el, tabId) {
|
|
155
50
|
el.setAttribute('active', tabId);
|
|
156
51
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Dispatch ty-tab-change event
|
|
159
|
-
*/
|
|
160
52
|
function dispatchTabChangeEvent(el, activeId, activeIndex, previousId, previousIndex) {
|
|
161
53
|
const event = new CustomEvent('ty-tab-change', {
|
|
162
54
|
detail: {
|
|
@@ -170,21 +62,11 @@ function dispatchTabChangeEvent(el, activeId, activeIndex, previousId, previousI
|
|
|
170
62
|
});
|
|
171
63
|
el.dispatchEvent(event);
|
|
172
64
|
}
|
|
173
|
-
// ============================================================================
|
|
174
|
-
// Event Handlers - Tab Button Click
|
|
175
|
-
// ============================================================================
|
|
176
|
-
/**
|
|
177
|
-
* Handle tab button click
|
|
178
|
-
*/
|
|
179
65
|
function handleTabClick(el, tabId, event) {
|
|
180
66
|
event.preventDefault();
|
|
181
67
|
event.stopPropagation();
|
|
182
|
-
// Set the active attribute (which will trigger state update)
|
|
183
68
|
setActiveTab(el, tabId);
|
|
184
69
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Cleanup existing event listeners
|
|
187
|
-
*/
|
|
188
70
|
function cleanupEventListeners(el) {
|
|
189
71
|
const handlers = eventHandlers.get(el);
|
|
190
72
|
if (!handlers)
|
|
@@ -192,7 +74,6 @@ function cleanupEventListeners(el) {
|
|
|
192
74
|
const shadowRoot = el.shadowRoot;
|
|
193
75
|
if (!shadowRoot)
|
|
194
76
|
return;
|
|
195
|
-
// Remove all tab click handlers
|
|
196
77
|
handlers.tabClickHandlers.forEach((handler, tabId) => {
|
|
197
78
|
const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
|
|
198
79
|
if (button) {
|
|
@@ -208,18 +89,12 @@ function cleanupEventListeners(el) {
|
|
|
208
89
|
handlers.tabClickHandlers.clear();
|
|
209
90
|
handlers.tabKeyClickHandlers.clear();
|
|
210
91
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Setup event listeners for tab button clicks
|
|
213
|
-
*/
|
|
214
92
|
function setupEventListeners(el, shadowRoot, tabs) {
|
|
215
|
-
// Clean up any existing listeners first
|
|
216
93
|
cleanupEventListeners(el);
|
|
217
|
-
// Initialize handlers storage
|
|
218
94
|
const handlers = {
|
|
219
95
|
tabClickHandlers: new Map(),
|
|
220
96
|
tabKeyClickHandlers: new Map(),
|
|
221
97
|
};
|
|
222
|
-
// Add click listener for each tab button
|
|
223
98
|
tabs.forEach((tab) => {
|
|
224
99
|
const tabId = getTabId(tab);
|
|
225
100
|
if (!tabId)
|
|
@@ -227,13 +102,6 @@ function setupEventListeners(el, shadowRoot, tabs) {
|
|
|
227
102
|
const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
|
|
228
103
|
if (button) {
|
|
229
104
|
const handler = (e) => handleTabClick(el, tabId, e);
|
|
230
|
-
// pointerdown: snappy mouse/touch activation (no native click delay).
|
|
231
|
-
// click, gated to event.detail === 0: native <button> keyboard
|
|
232
|
-
// activation (Enter/Space) fires 'click' with detail 0 — real mouse
|
|
233
|
-
// clicks report detail >= 1 — so this covers keyboard users without
|
|
234
|
-
// double-firing handleTabClick on an actual pointer click (which
|
|
235
|
-
// already fired via pointerdown above). Without this, keyboard-only
|
|
236
|
-
// users could Tab to a tab button but had no way to activate it.
|
|
237
105
|
const keyClickHandler = (e) => {
|
|
238
106
|
if (e.detail === 0)
|
|
239
107
|
handleTabClick(el, tabId, e);
|
|
@@ -244,15 +112,8 @@ function setupEventListeners(el, shadowRoot, tabs) {
|
|
|
244
112
|
handlers.tabKeyClickHandlers.set(tabId, keyClickHandler);
|
|
245
113
|
}
|
|
246
114
|
});
|
|
247
|
-
// Store handlers for cleanup
|
|
248
115
|
eventHandlers.set(el, handlers);
|
|
249
116
|
}
|
|
250
|
-
// ============================================================================
|
|
251
|
-
// Transform & Positioning Updates
|
|
252
|
-
// ============================================================================
|
|
253
|
-
/**
|
|
254
|
-
* Update the transform on panels-wrapper based on active index and measured width
|
|
255
|
-
*/
|
|
256
117
|
function updateTransform(el, activeIndex) {
|
|
257
118
|
const shadowRoot = el.shadowRoot;
|
|
258
119
|
if (!shadowRoot)
|
|
@@ -260,15 +121,10 @@ function updateTransform(el, activeIndex) {
|
|
|
260
121
|
const panelsWrapper = shadowRoot.querySelector('.panels-wrapper');
|
|
261
122
|
if (!panelsWrapper)
|
|
262
123
|
return;
|
|
263
|
-
// Measure the actual width of the container
|
|
264
124
|
const containerWidth = el.offsetWidth;
|
|
265
125
|
const offsetPx = activeIndex * containerWidth;
|
|
266
|
-
// Apply transform directly in pixels
|
|
267
126
|
panelsWrapper.style.transform = `translateX(-${offsetPx}px)`;
|
|
268
127
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Update ARIA attributes on tab buttons without re-rendering
|
|
271
|
-
*/
|
|
272
128
|
function updateAriaAttributes(el, shadowRoot, activeId) {
|
|
273
129
|
const tabs = getChildTabs(el);
|
|
274
130
|
tabs.forEach((tab, idx) => {
|
|
@@ -280,9 +136,7 @@ function updateAriaAttributes(el, shadowRoot, activeId) {
|
|
|
280
136
|
if (button) {
|
|
281
137
|
button.setAttribute('aria-selected', String(isActive));
|
|
282
138
|
button.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
283
|
-
// Add data-active attribute for styling slotted content
|
|
284
139
|
button.setAttribute('data-active', String(isActive));
|
|
285
|
-
// Also set data-active on the slotted label element in light DOM
|
|
286
140
|
const slottedLabel = el.querySelector(`[slot='label-${tabId}']`);
|
|
287
141
|
if (slottedLabel) {
|
|
288
142
|
slottedLabel.setAttribute('data-active', String(isActive));
|
|
@@ -290,9 +144,6 @@ function updateAriaAttributes(el, shadowRoot, activeId) {
|
|
|
290
144
|
}
|
|
291
145
|
});
|
|
292
146
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Update pointer-events, opacity, and data-active on tab panels without re-rendering
|
|
295
|
-
*/
|
|
296
147
|
function updatePanelInteraction(el, activeId) {
|
|
297
148
|
const tabs = getChildTabs(el);
|
|
298
149
|
tabs.forEach((tab) => {
|
|
@@ -300,7 +151,6 @@ function updatePanelInteraction(el, activeId) {
|
|
|
300
151
|
if (!tabId)
|
|
301
152
|
return;
|
|
302
153
|
const isActive = tabId === activeId;
|
|
303
|
-
// Set data-active attribute for framework conditional rendering
|
|
304
154
|
tab.setAttribute('data-active', String(isActive));
|
|
305
155
|
if (isActive) {
|
|
306
156
|
tab.style.pointerEvents = 'auto';
|
|
@@ -312,17 +162,12 @@ function updatePanelInteraction(el, activeId) {
|
|
|
312
162
|
}
|
|
313
163
|
});
|
|
314
164
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Calculate marker position and dimensions based on active tab button.
|
|
317
|
-
* Returns position object with left, top, width, height in pixels, or null if button not found.
|
|
318
|
-
*/
|
|
319
165
|
function calculateMarkerPosition(el, shadowRoot, activeId) {
|
|
320
166
|
const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
|
|
321
167
|
const buttonsContainer = shadowRoot.querySelector('.tab-buttons');
|
|
322
168
|
if (!button || !buttonsContainer)
|
|
323
169
|
return null;
|
|
324
170
|
const buttonRect = button.getBoundingClientRect();
|
|
325
|
-
// Use offset properties for position relative to container (accounts for padding)
|
|
326
171
|
const left = button.offsetLeft;
|
|
327
172
|
const top = button.offsetTop;
|
|
328
173
|
return {
|
|
@@ -332,9 +177,6 @@ function calculateMarkerPosition(el, shadowRoot, activeId) {
|
|
|
332
177
|
height: buttonRect.height,
|
|
333
178
|
};
|
|
334
179
|
}
|
|
335
|
-
/**
|
|
336
|
-
* Update marker wrapper position and dimensions to match active tab button
|
|
337
|
-
*/
|
|
338
180
|
function updateMarker(el, activeId) {
|
|
339
181
|
const shadowRoot = el.shadowRoot;
|
|
340
182
|
if (!shadowRoot)
|
|
@@ -345,9 +187,6 @@ function updateMarker(el, activeId) {
|
|
|
345
187
|
const position = calculateMarkerPosition(el, shadowRoot, activeId);
|
|
346
188
|
if (!position)
|
|
347
189
|
return;
|
|
348
|
-
// Snap without animating when the marker was never positioned, or was
|
|
349
|
-
// positioned while the tabs were hidden (zero-size rects) — otherwise it
|
|
350
|
-
// visibly glides in from 0,0 on first display.
|
|
351
190
|
const snap = !marker.style.left || marker.offsetWidth === 0;
|
|
352
191
|
if (snap)
|
|
353
192
|
marker.style.transition = 'none';
|
|
@@ -356,20 +195,10 @@ function updateMarker(el, activeId) {
|
|
|
356
195
|
marker.style.width = `${position.width}px`;
|
|
357
196
|
marker.style.height = `${position.height}px`;
|
|
358
197
|
if (snap) {
|
|
359
|
-
marker.offsetWidth;
|
|
198
|
+
marker.offsetWidth;
|
|
360
199
|
marker.style.transition = '';
|
|
361
200
|
}
|
|
362
201
|
}
|
|
363
|
-
/**
|
|
364
|
-
* Update only the active tab state without re-rendering DOM.
|
|
365
|
-
* Called from `attributeChangedCallback` when the `active` attribute changes.
|
|
366
|
-
*
|
|
367
|
-
* `previousId` MUST be the value the attribute held *before* it was changed —
|
|
368
|
-
* never call `getActiveTabId(el, ...)` here, because the attribute has already
|
|
369
|
-
* been updated to `tabId` by the time this runs. Reading it back would always
|
|
370
|
-
* yield the new value, making the change-detection guard below trip on every
|
|
371
|
-
* click and silently suppress `ty-tab-change` dispatch.
|
|
372
|
-
*/
|
|
373
202
|
function updateActiveTabState(el, tabId, previousId) {
|
|
374
203
|
const tabs = getChildTabs(el);
|
|
375
204
|
const shadowRoot = el.shadowRoot;
|
|
@@ -377,56 +206,36 @@ function updateActiveTabState(el, tabId, previousId) {
|
|
|
377
206
|
return;
|
|
378
207
|
const currentIndex = previousId ? findTabIndex(tabs, previousId) : undefined;
|
|
379
208
|
const newIndex = findTabIndex(tabs, tabId);
|
|
380
|
-
// Only update if different tab and valid
|
|
381
209
|
if (previousId === tabId || newIndex === undefined)
|
|
382
210
|
return;
|
|
383
|
-
// Update CSS variable for transform
|
|
384
211
|
el.style.setProperty('--active-index', String(newIndex));
|
|
385
|
-
// Update transform directly
|
|
386
212
|
updateTransform(el, newIndex);
|
|
387
|
-
// Update ARIA attributes on buttons
|
|
388
213
|
updateAriaAttributes(el, shadowRoot, tabId);
|
|
389
|
-
// Update pointer-events on panels
|
|
390
214
|
updatePanelInteraction(el, tabId);
|
|
391
|
-
// Update marker position to match new active tab
|
|
392
215
|
updateMarker(el, tabId);
|
|
393
|
-
// Reset scroll position of new active panel
|
|
394
216
|
const newPanel = tabs[newIndex];
|
|
395
217
|
if (newPanel?.resetScroll) {
|
|
396
218
|
newPanel.resetScroll();
|
|
397
219
|
}
|
|
398
|
-
// Dispatch change event
|
|
399
220
|
dispatchTabChangeEvent(el, tabId, newIndex, previousId, currentIndex ?? null);
|
|
400
221
|
}
|
|
401
|
-
// ============================================================================
|
|
402
|
-
// ResizeObserver for Responsive Width
|
|
403
|
-
// ============================================================================
|
|
404
|
-
/**
|
|
405
|
-
* Setup ResizeObserver for percentage widths and marker updates
|
|
406
|
-
*/
|
|
407
222
|
function setupResizeObserver(el) {
|
|
408
|
-
// Clean up old observer
|
|
409
223
|
const oldObserver = resizeObservers.get(el);
|
|
410
224
|
if (oldObserver) {
|
|
411
225
|
oldObserver.disconnect();
|
|
412
226
|
}
|
|
413
|
-
// Observe unconditionally (not just for '%' widths) — overflow can happen
|
|
414
|
-
// at any fixed width too, and needs to react to container resizes.
|
|
415
227
|
const observer = new ResizeObserver((entries) => {
|
|
416
228
|
const entry = entries[0];
|
|
417
229
|
const { width } = getTabsAttributes(el);
|
|
418
230
|
const tabs = getChildTabs(el);
|
|
419
231
|
const activeId = getActiveTabId(el, tabs);
|
|
420
232
|
const activeIndex = activeId ? findTabIndex(tabs, activeId) : 0;
|
|
421
|
-
// Update CSS variable with measured width (percentage widths only)
|
|
422
233
|
if (width.includes('%')) {
|
|
423
|
-
el.style.setProperty('--tabs-width', `${entry.contentRect.width}px`);
|
|
234
|
+
el.style.setProperty('--tabs-page-width', `${entry.contentRect.width}px`);
|
|
424
235
|
}
|
|
425
|
-
// Update transform with new width
|
|
426
236
|
if (activeIndex !== undefined) {
|
|
427
237
|
updateTransform(el, activeIndex);
|
|
428
238
|
}
|
|
429
|
-
// Update marker position (tab button positions may have changed)
|
|
430
239
|
if (activeId) {
|
|
431
240
|
updateMarker(el, activeId);
|
|
432
241
|
}
|
|
@@ -435,9 +244,6 @@ function setupResizeObserver(el) {
|
|
|
435
244
|
observer.observe(el);
|
|
436
245
|
resizeObservers.set(el, observer);
|
|
437
246
|
}
|
|
438
|
-
/**
|
|
439
|
-
* Cleanup ResizeObserver
|
|
440
|
-
*/
|
|
441
247
|
function cleanupResizeObserver(el) {
|
|
442
248
|
const observer = resizeObservers.get(el);
|
|
443
249
|
if (observer) {
|
|
@@ -445,14 +251,6 @@ function cleanupResizeObserver(el) {
|
|
|
445
251
|
resizeObservers.delete(el);
|
|
446
252
|
}
|
|
447
253
|
}
|
|
448
|
-
// ============================================================================
|
|
449
|
-
// Rendering Functions
|
|
450
|
-
// ============================================================================
|
|
451
|
-
/**
|
|
452
|
-
* Generate HTML for tab buttons using slots for rich labels.
|
|
453
|
-
* Slots are looked up as direct children of tabs-el with slot='label-{tab-id}'.
|
|
454
|
-
* Also renders marker wrapper with optional default underline (only if no custom marker).
|
|
455
|
-
*/
|
|
456
254
|
function renderTabButtons(tabsEl, tabs, activeId) {
|
|
457
255
|
const buttons = tabs.map((tab) => {
|
|
458
256
|
const tabId = getTabId(tab);
|
|
@@ -485,10 +283,6 @@ function renderTabButtons(tabsEl, tabs, activeId) {
|
|
|
485
283
|
</div>
|
|
486
284
|
`;
|
|
487
285
|
}
|
|
488
|
-
/**
|
|
489
|
-
* Generate HTML for ONLY the button elements (no marker wrapper).
|
|
490
|
-
* Used during smart updates to preserve the marker wrapper.
|
|
491
|
-
*/
|
|
492
286
|
function renderButtonsOnly(tabsEl, tabs, activeId) {
|
|
493
287
|
return tabs.map((tab) => {
|
|
494
288
|
const tabId = getTabId(tab);
|
|
@@ -512,11 +306,7 @@ function renderButtonsOnly(tabsEl, tabs, activeId) {
|
|
|
512
306
|
</button>`;
|
|
513
307
|
}).join('');
|
|
514
308
|
}
|
|
515
|
-
// ============================================================================
|
|
516
|
-
// Overflow Handling (collapse tabs that don't fit into a "more" menu)
|
|
517
|
-
// ============================================================================
|
|
518
309
|
const OVERFLOW_TRIGGER_WIDTH = 40;
|
|
519
|
-
/** Build a display label for a hidden tab's overflow-menu entry. */
|
|
520
310
|
function getTabMenuLabel(tabsEl, tab) {
|
|
521
311
|
const tabId = getTabId(tab);
|
|
522
312
|
if (tabId && getTabLabelType(tabsEl, tab) === 'slot') {
|
|
@@ -525,15 +315,9 @@ function getTabMenuLabel(tabsEl, tab) {
|
|
|
525
315
|
}
|
|
526
316
|
return tab.getAttribute('label') || 'Tab';
|
|
527
317
|
}
|
|
528
|
-
/** Remove the "more" trigger + its popup, if present. */
|
|
529
318
|
function removeOverflowMenu(shadowRoot) {
|
|
530
319
|
shadowRoot.querySelector('.tab-overflow-trigger')?.remove();
|
|
531
320
|
}
|
|
532
|
-
/**
|
|
533
|
-
* Build the "more" trigger button + its ty-popup menu for the given
|
|
534
|
-
* (already-hidden) tab buttons, and append it to the buttons container.
|
|
535
|
-
* Reuses ty-popup instead of a second floating-menu implementation.
|
|
536
|
-
*/
|
|
537
321
|
function renderOverflowMenu(el, container, hiddenButtons, allTabs) {
|
|
538
322
|
if (hiddenButtons.length === 0)
|
|
539
323
|
return;
|
|
@@ -573,12 +357,6 @@ function renderOverflowMenu(el, container, hiddenButtons, allTabs) {
|
|
|
573
357
|
trigger.appendChild(popup);
|
|
574
358
|
container.appendChild(trigger);
|
|
575
359
|
}
|
|
576
|
-
/**
|
|
577
|
-
* Measure the tab buttons against the available width and collapse whatever
|
|
578
|
-
* doesn't fit behind a "more" trigger + popup menu. The active tab is always
|
|
579
|
-
* kept visible — if it would be the one to overflow, the last visible button
|
|
580
|
-
* is hidden in its place instead.
|
|
581
|
-
*/
|
|
582
360
|
function updateOverflow(el) {
|
|
583
361
|
const shadowRoot = el.shadowRoot;
|
|
584
362
|
if (!shadowRoot)
|
|
@@ -592,6 +370,8 @@ function updateOverflow(el) {
|
|
|
592
370
|
if (buttons.length === 0)
|
|
593
371
|
return;
|
|
594
372
|
const available = container.clientWidth;
|
|
373
|
+
if (available <= 0)
|
|
374
|
+
return;
|
|
595
375
|
let total = 0;
|
|
596
376
|
let cutoff = buttons.length;
|
|
597
377
|
for (let i = 0; i < buttons.length; i++) {
|
|
@@ -602,8 +382,7 @@ function updateOverflow(el) {
|
|
|
602
382
|
}
|
|
603
383
|
}
|
|
604
384
|
if (cutoff >= buttons.length)
|
|
605
|
-
return;
|
|
606
|
-
// Recompute the cutoff leaving room for the "more" trigger itself.
|
|
385
|
+
return;
|
|
607
386
|
total = 0;
|
|
608
387
|
cutoff = buttons.length;
|
|
609
388
|
for (let i = 0; i < buttons.length; i++) {
|
|
@@ -626,10 +405,6 @@ function updateOverflow(el) {
|
|
|
626
405
|
buttons.forEach((b) => b.classList.toggle('overflow-hidden', hidden.has(b)));
|
|
627
406
|
renderOverflowMenu(el, container, buttons.filter((b) => hidden.has(b)), tabs);
|
|
628
407
|
}
|
|
629
|
-
/**
|
|
630
|
-
* Render the tabs container with buttons and panel viewport.
|
|
631
|
-
* Smart rendering: checks if structure exists and only updates when needed.
|
|
632
|
-
*/
|
|
633
408
|
function render(el) {
|
|
634
409
|
const shadowRoot = el.shadowRoot;
|
|
635
410
|
if (!shadowRoot)
|
|
@@ -638,16 +413,6 @@ function render(el) {
|
|
|
638
413
|
const tabs = getChildTabs(el);
|
|
639
414
|
const activeId = getActiveTabId(el, tabs);
|
|
640
415
|
const activeIndex = activeId ? (findTabIndex(tabs, activeId) ?? 0) : 0;
|
|
641
|
-
// Each <ty-tab> IS its tabpanel (no separate wrapper — there's just one
|
|
642
|
-
// catch-all <slot> in the carousel viewport below). role/aria-labelledby/
|
|
643
|
-
// tabindex go directly on the light-DOM host; its `id` attribute is ALSO
|
|
644
|
-
// its tabId (getTabId reads it directly), so it's never touched here.
|
|
645
|
-
// No aria-controls on the tab BUTTON pointing at this: it lives in the
|
|
646
|
-
// shadow root while the panel is in light DOM, and ARIA id-references
|
|
647
|
-
// don't resolve across a shadow-tree boundary — axe's aria-valid-attr-value
|
|
648
|
-
// correctly flags that as invalid even though getElementById can find the
|
|
649
|
-
// target. role=tab + aria-selected + aria-labelledby (this direction) is
|
|
650
|
-
// what's actually load-bearing; caught by axe once ty-tabs got coverage.
|
|
651
416
|
tabs.forEach((tab) => {
|
|
652
417
|
const tabId = getTabId(tab);
|
|
653
418
|
if (!tabId)
|
|
@@ -656,63 +421,48 @@ function render(el) {
|
|
|
656
421
|
tab.setAttribute('aria-labelledby', `tab-${tabId}`);
|
|
657
422
|
tab.setAttribute('tabindex', '0');
|
|
658
423
|
});
|
|
659
|
-
// Check if structure already exists
|
|
660
424
|
const existingContainer = shadowRoot.querySelector('.tabs-container');
|
|
661
425
|
const existingButtons = shadowRoot.querySelector('.tab-buttons');
|
|
662
426
|
const existingViewport = shadowRoot.querySelector('.panels-viewport');
|
|
663
|
-
// Ensure styles are loaded
|
|
664
427
|
ensureStyles(shadowRoot, { css: tabsStyles, id: 'ty-tabs' });
|
|
665
|
-
// Dev warning for too many tabs
|
|
666
428
|
if (tabs.length > 7) {
|
|
667
429
|
console.warn(`[ty-tabs] More than 7 tabs detected (${tabs.length} tabs). ` +
|
|
668
430
|
'This may cause overflow and poor UX. ' +
|
|
669
431
|
'Consider using sidebar navigation, accordion menu, or other patterns.');
|
|
670
432
|
}
|
|
671
|
-
// Set CSS variables for dimensions
|
|
672
433
|
el.style.setProperty('--tabs-width', width.includes('%') ? '100%' : width);
|
|
673
434
|
el.style.setProperty('--tabs-height', height);
|
|
674
435
|
el.style.setProperty('--active-index', String(activeIndex));
|
|
675
436
|
if (existingContainer && existingButtons && existingViewport) {
|
|
676
|
-
// === SMART UPDATE: Structure exists, only update what changed ===
|
|
677
|
-
// Update placement if changed
|
|
678
437
|
existingContainer.setAttribute('data-placement', placement);
|
|
679
|
-
// SMART UPDATE: Preserve marker wrapper, only update buttons
|
|
680
438
|
existingButtons.querySelector('.tab-overflow-trigger')?.remove();
|
|
681
439
|
const allButtons = existingButtons.querySelectorAll('.tab-button');
|
|
682
440
|
allButtons.forEach(button => button.remove());
|
|
683
|
-
// Generate and append new buttons after the marker wrapper
|
|
684
441
|
const buttonsHtml = renderButtonsOnly(el, tabs, activeId);
|
|
685
442
|
const tempDiv = document.createElement('div');
|
|
686
443
|
tempDiv.innerHTML = buttonsHtml;
|
|
687
444
|
Array.from(tempDiv.children).forEach(button => {
|
|
688
445
|
existingButtons.appendChild(button);
|
|
689
446
|
});
|
|
690
|
-
// Re-setup event listeners (buttons were recreated)
|
|
691
447
|
setupEventListeners(el, shadowRoot, tabs);
|
|
692
|
-
// Update ARIA and data-active attributes on initial render
|
|
693
448
|
updateAriaAttributes(el, shadowRoot, activeId || '');
|
|
694
|
-
// Measure button height and update marker after update
|
|
695
449
|
requestAnimationFrame(() => {
|
|
696
450
|
const buttons = shadowRoot.querySelector('.tab-buttons');
|
|
697
451
|
if (buttons) {
|
|
698
452
|
const buttonsHeight = buttons.offsetHeight;
|
|
699
453
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
700
454
|
}
|
|
701
|
-
// Update transform with current active index
|
|
702
455
|
updateTransform(el, activeIndex);
|
|
703
|
-
// Update marker position to match active tab
|
|
704
456
|
if (activeId) {
|
|
705
457
|
updateMarker(el, activeId);
|
|
706
458
|
}
|
|
707
459
|
updateOverflow(el);
|
|
708
460
|
});
|
|
709
|
-
// Update panel interaction states
|
|
710
461
|
if (activeId) {
|
|
711
462
|
updatePanelInteraction(el, activeId);
|
|
712
463
|
}
|
|
713
464
|
}
|
|
714
465
|
else {
|
|
715
|
-
// === FULL RENDER: First time or structure missing ===
|
|
716
466
|
shadowRoot.innerHTML = `
|
|
717
467
|
<div class="tabs-container" data-placement="${placement}">
|
|
718
468
|
${renderTabButtons(el, tabs, activeId)}
|
|
@@ -723,32 +473,24 @@ function render(el) {
|
|
|
723
473
|
</div>
|
|
724
474
|
</div>
|
|
725
475
|
`;
|
|
726
|
-
// Measure button height, update transform and marker after render
|
|
727
476
|
requestAnimationFrame(() => {
|
|
728
477
|
const buttons = shadowRoot.querySelector('.tab-buttons');
|
|
729
478
|
if (buttons) {
|
|
730
479
|
const buttonsHeight = buttons.offsetHeight;
|
|
731
480
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
732
481
|
}
|
|
733
|
-
// Update transform with measured width
|
|
734
482
|
updateTransform(el, activeIndex);
|
|
735
|
-
// Update marker position to match active tab
|
|
736
483
|
if (activeId) {
|
|
737
484
|
updateMarker(el, activeId);
|
|
738
485
|
}
|
|
739
486
|
updateOverflow(el);
|
|
740
487
|
});
|
|
741
|
-
// Setup event listeners
|
|
742
488
|
setupEventListeners(el, shadowRoot, tabs);
|
|
743
|
-
// Update ARIA and data-active attributes on initial render
|
|
744
489
|
updateAriaAttributes(el, shadowRoot, activeId || '');
|
|
745
|
-
// Setup ResizeObserver for responsive width
|
|
746
490
|
setupResizeObserver(el);
|
|
747
|
-
// Update tab panel states
|
|
748
491
|
if (activeId) {
|
|
749
492
|
updatePanelInteraction(el, activeId);
|
|
750
493
|
}
|
|
751
|
-
// Listen for marker slot changes to hide default marker
|
|
752
494
|
const markerSlot = shadowRoot.querySelector('slot[name="marker"]');
|
|
753
495
|
if (markerSlot) {
|
|
754
496
|
const updateDefaultMarkerVisibility = () => {
|
|
@@ -759,60 +501,55 @@ function render(el) {
|
|
|
759
501
|
}
|
|
760
502
|
};
|
|
761
503
|
markerSlot.addEventListener('slotchange', updateDefaultMarkerVisibility);
|
|
762
|
-
// Check immediately in case content is already slotted
|
|
763
504
|
updateDefaultMarkerVisibility();
|
|
764
505
|
}
|
|
765
506
|
}
|
|
766
507
|
}
|
|
767
|
-
/**
|
|
768
|
-
* Cleanup when tabs component is disconnected
|
|
769
|
-
*/
|
|
770
508
|
function cleanup(el) {
|
|
771
509
|
cleanupEventListeners(el);
|
|
772
510
|
cleanupResizeObserver(el);
|
|
773
511
|
}
|
|
774
|
-
// ============================================================================
|
|
775
|
-
// Component Definition
|
|
776
|
-
// ============================================================================
|
|
777
|
-
/**
|
|
778
|
-
* TyTabs Web Component
|
|
779
|
-
*/
|
|
780
512
|
export class TyTabs extends HTMLElement {
|
|
781
|
-
/** Observed attributes */
|
|
782
513
|
static get observedAttributes() {
|
|
783
514
|
return ['width', 'height', 'active', 'placement'];
|
|
784
515
|
}
|
|
785
516
|
constructor() {
|
|
786
517
|
super();
|
|
518
|
+
this._childObserver = null;
|
|
519
|
+
this._childRenderQueued = false;
|
|
787
520
|
this.attachShadow({ mode: 'open' });
|
|
788
521
|
}
|
|
789
522
|
connectedCallback() {
|
|
790
523
|
render(this);
|
|
524
|
+
this._childObserver = new MutationObserver(() => {
|
|
525
|
+
if (this._childRenderQueued)
|
|
526
|
+
return;
|
|
527
|
+
this._childRenderQueued = true;
|
|
528
|
+
queueMicrotask(() => {
|
|
529
|
+
this._childRenderQueued = false;
|
|
530
|
+
if (this.isConnected)
|
|
531
|
+
render(this);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
this._childObserver.observe(this, { childList: true });
|
|
791
535
|
}
|
|
792
536
|
disconnectedCallback() {
|
|
537
|
+
this._childObserver?.disconnect();
|
|
538
|
+
this._childObserver = null;
|
|
793
539
|
cleanup(this);
|
|
794
540
|
}
|
|
795
541
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
796
|
-
// Smart rendering: only full render when structural attributes change
|
|
797
542
|
if (name === 'active') {
|
|
798
|
-
// Active tab changed - update state, then do smart render
|
|
799
543
|
if (newValue) {
|
|
800
|
-
// Pass `oldValue` explicitly so the change-detection inside
|
|
801
|
-
// `updateActiveTabState` compares against the previous value
|
|
802
|
-
// rather than re-reading the (already updated) attribute.
|
|
803
544
|
updateActiveTabState(this, newValue, oldValue);
|
|
804
545
|
}
|
|
805
|
-
// Always call render after active change to update button states
|
|
806
546
|
render(this);
|
|
807
547
|
}
|
|
808
548
|
else {
|
|
809
|
-
// Other attributes changed (width, height, placement) - full render
|
|
810
549
|
render(this);
|
|
811
550
|
}
|
|
812
551
|
}
|
|
813
552
|
}
|
|
814
|
-
// Register the custom element
|
|
815
553
|
if (!customElements.get('ty-tabs')) {
|
|
816
554
|
customElements.define('ty-tabs', TyTabs);
|
|
817
555
|
}
|
|
818
|
-
//# sourceMappingURL=tabs.js.map
|