@nuvia-ui/components 4.0.1
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/package.json +27 -0
- package/src/ds-accordion/ds-accordion-item.js +288 -0
- package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
- package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
- package/src/ds-accordion/ds-accordion.js +68 -0
- package/src/ds-accordion/ds-accordion.stories.js +118 -0
- package/src/ds-accordion/ds-accordion.test.js +146 -0
- package/src/ds-accordion/index.js +2 -0
- package/src/ds-action-bar/ds-action-bar.js +116 -0
- package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
- package/src/ds-action-bar/ds-action-bar.test.js +64 -0
- package/src/ds-action-bar/index.js +1 -0
- package/src/ds-alert/ds-alert.a11y.test.js +151 -0
- package/src/ds-alert/ds-alert.js +223 -0
- package/src/ds-alert/ds-alert.mdx +142 -0
- package/src/ds-alert/ds-alert.stories.js +166 -0
- package/src/ds-alert/ds-alert.test.js +256 -0
- package/src/ds-alert/index.js +1 -0
- package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
- package/src/ds-avatar/ds-avatar.js +216 -0
- package/src/ds-avatar/ds-avatar.stories.js +120 -0
- package/src/ds-avatar/ds-avatar.test.js +83 -0
- package/src/ds-avatar/index.js +1 -0
- package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
- package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
- package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
- package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
- package/src/ds-avatar-extended/index.js +1 -0
- package/src/ds-banner/ds-banner.a11y.test.js +51 -0
- package/src/ds-banner/ds-banner.js +233 -0
- package/src/ds-banner/ds-banner.stories.js +185 -0
- package/src/ds-banner/ds-banner.test.js +116 -0
- package/src/ds-banner/index.js +1 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
- package/src/ds-button/ds-button.a11y.test.js +49 -0
- package/src/ds-button/ds-button.js +205 -0
- package/src/ds-button/ds-button.mdx +141 -0
- package/src/ds-button/ds-button.stories.js +152 -0
- package/src/ds-button/ds-button.test.js +62 -0
- package/src/ds-button/index.js +1 -0
- package/src/ds-button-group/ds-button-group.js +82 -0
- package/src/ds-button-group/ds-button-group.mdx +39 -0
- package/src/ds-button-group/ds-button-group.stories.js +47 -0
- package/src/ds-button-group/ds-button-group.test.js +47 -0
- package/src/ds-button-group/index.js +1 -0
- package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
- package/src/ds-checkbox/ds-checkbox.js +271 -0
- package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
- package/src/ds-checkbox/ds-checkbox.test.js +191 -0
- package/src/ds-checkbox/index.js +1 -0
- package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
- package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
- package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
- package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
- package/src/ds-checkbox-group/index.js +1 -0
- package/src/ds-dialog/ds-dialog.js +466 -0
- package/src/ds-dialog/ds-dialog.stories.js +274 -0
- package/src/ds-dialog/ds-dialog.test.js +441 -0
- package/src/ds-dialog/index.js +1 -0
- package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
- package/src/ds-dropdown/ds-dropdown.js +891 -0
- package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
- package/src/ds-dropdown/ds-dropdown.test.js +268 -0
- package/src/ds-dropdown/index.js +1 -0
- package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
- package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
- package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
- package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
- package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
- package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
- package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
- package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
- package/src/ds-file-uploader/ds-file-uploader.js +442 -0
- package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
- package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
- package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
- package/src/ds-header/ds-header.a11y.test.js +38 -0
- package/src/ds-header/ds-header.js +149 -0
- package/src/ds-header/ds-header.stories.js +63 -0
- package/src/ds-header/ds-header.test.js +52 -0
- package/src/ds-header/index.js +1 -0
- package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
- package/src/ds-header-nav/ds-header-nav.js +114 -0
- package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
- package/src/ds-header-nav/ds-header-nav.test.js +93 -0
- package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
- package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
- package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
- package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
- package/src/ds-icon/ds-icon.a11y.test.js +49 -0
- package/src/ds-icon/ds-icon.js +75 -0
- package/src/ds-icon/ds-icon.mdx +36 -0
- package/src/ds-icon/ds-icon.stories.js +88 -0
- package/src/ds-icon/ds-icon.test.js +97 -0
- package/src/ds-icon/index.js +1 -0
- package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
- package/src/ds-icon-button/ds-icon-button.js +224 -0
- package/src/ds-icon-button/ds-icon-button.mdx +131 -0
- package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
- package/src/ds-icon-button/ds-icon-button.test.js +90 -0
- package/src/ds-icon-button/index.js +1 -0
- package/src/ds-input/ds-input.a11y.test.js +145 -0
- package/src/ds-input/ds-input.js +645 -0
- package/src/ds-input/ds-input.mdx +251 -0
- package/src/ds-input/ds-input.stories.js +298 -0
- package/src/ds-input/ds-input.test.js +792 -0
- package/src/ds-input/index.js +1 -0
- package/src/ds-link/ds-link.js +111 -0
- package/src/ds-link/ds-link.stories.js +56 -0
- package/src/ds-link/ds-link.test.js +74 -0
- package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
- package/src/ds-list-item/ds-list-item.js +292 -0
- package/src/ds-list-item/ds-list-item.stories.js +101 -0
- package/src/ds-list-item/ds-list-item.test.js +63 -0
- package/src/ds-menu/ds-menu.js +30 -0
- package/src/ds-menu/ds-menu.stories.js +120 -0
- package/src/ds-menu/ds-menu.test.js +123 -0
- package/src/ds-menu-group/ds-menu-group.js +101 -0
- package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
- package/src/ds-nav-item/ds-nav-item.js +307 -0
- package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.test.js +169 -0
- package/src/ds-nav-item/index.js +1 -0
- package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
- package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
- package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
- package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
- package/src/ds-nav-vertical/index.js +1 -0
- package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
- package/src/ds-pagination/ds-pagination.js +232 -0
- package/src/ds-pagination/ds-pagination.stories.js +63 -0
- package/src/ds-pagination/ds-pagination.test.js +141 -0
- package/src/ds-pagination/index.js +1 -0
- package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
- package/src/ds-progress-bar/ds-progress-bar.js +81 -0
- package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
- package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
- package/src/ds-radio/ds-radio.a11y.test.js +69 -0
- package/src/ds-radio/ds-radio.js +240 -0
- package/src/ds-radio/ds-radio.stories.js +102 -0
- package/src/ds-radio/ds-radio.test.js +114 -0
- package/src/ds-radio/index.js +1 -0
- package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
- package/src/ds-radio-group/ds-radio-group.js +257 -0
- package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
- package/src/ds-radio-group/ds-radio-group.test.js +194 -0
- package/src/ds-radio-group/index.js +1 -0
- package/src/ds-rich-list/ds-rich-list.js +246 -0
- package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
- package/src/ds-rich-list/ds-rich-list.test.js +293 -0
- package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
- package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
- package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
- package/src/ds-slider/ds-slider.js +399 -0
- package/src/ds-slider/ds-slider.stories.js +107 -0
- package/src/ds-slider/ds-slider.test.js +308 -0
- package/src/ds-spinner/ds-spinner.js +173 -0
- package/src/ds-spinner/ds-spinner.stories.js +52 -0
- package/src/ds-spinner/ds-spinner.test.js +50 -0
- package/src/ds-status-border/ds-status-border.js +88 -0
- package/src/ds-status-border/ds-status-border.stories.js +242 -0
- package/src/ds-status-border/ds-status-border.test.js +168 -0
- package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
- package/src/ds-stepper/ds-stepper.js +207 -0
- package/src/ds-stepper/ds-stepper.stories.js +530 -0
- package/src/ds-stepper/ds-stepper.test.js +311 -0
- package/src/ds-stepper-item/ds-stepper-item.js +485 -0
- package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
- package/src/ds-switch/ds-switch.js +348 -0
- package/src/ds-switch/ds-switch.stories.js +145 -0
- package/src/ds-switch/ds-switch.test.js +226 -0
- package/src/ds-switch/index.js +1 -0
- package/src/ds-tab-item/ds-tab-item.js +341 -0
- package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
- package/src/ds-tabs/ds-tab-panel.js +48 -0
- package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
- package/src/ds-tabs/ds-tabs.js +180 -0
- package/src/ds-tabs/ds-tabs.stories.js +152 -0
- package/src/ds-tabs/ds-tabs.test.js +306 -0
- package/src/ds-tabs/index.js +3 -0
- package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
- package/src/ds-tag-action/ds-tag-action.js +185 -0
- package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
- package/src/ds-tag-action/ds-tag-action.test.js +44 -0
- package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
- package/src/ds-tag-removable/ds-tag-removable.js +146 -0
- package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
- package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
- package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
- package/src/ds-tag-status/ds-tag-status.js +164 -0
- package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
- package/src/ds-tag-status/ds-tag-status.test.js +140 -0
- package/src/ds-tag-status/index.js +1 -0
- package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
- package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
- package/src/ds-textarea/ds-textarea.js +505 -0
- package/src/ds-textarea/ds-textarea.stories.js +335 -0
- package/src/ds-textarea/ds-textarea.test.js +218 -0
- package/src/ds-textarea/index.js +1 -0
- package/src/ds-thumbnail/ds-thumbnail.js +207 -0
- package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
- package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
- package/src/ds-toast/ds-toast-provider.js +110 -0
- package/src/ds-toast/ds-toast.a11y.test.js +34 -0
- package/src/ds-toast/ds-toast.js +243 -0
- package/src/ds-toast/ds-toast.stories.js +143 -0
- package/src/ds-toast/ds-toast.test.js +93 -0
- package/src/ds-toast/index.js +2 -0
- package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
- package/src/ds-tooltip/ds-tooltip.js +217 -0
- package/src/ds-tooltip/ds-tooltip.mdx +75 -0
- package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
- package/src/ds-tooltip/ds-tooltip.test.js +191 -0
- package/src/ds-tooltip/index.js +1 -0
- package/src/ds-tooltip/positioner.js +117 -0
- package/src/index.js +50 -0
- package/src/mixins/field-label.mixin.js +113 -0
- package/src/mixins/field-message.mixin.js +66 -0
- package/src/token-provider/index.js +1 -0
- package/src/token-provider/token-provider.a11y.test.js +44 -0
- package/src/token-provider/token-provider.js +85 -0
- package/src/token-provider/token-provider.stories.js +105 -0
- package/src/token-provider/token-provider.test.js +134 -0
- package/src/utils/number-input.utils.js +42 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
3
|
+
import '../ds-icon/ds-icon.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @element ds-tab-item
|
|
7
|
+
* @summary A single tab item.
|
|
8
|
+
*
|
|
9
|
+
* @slot - Default slot for label text.
|
|
10
|
+
* @slot icon - Decorative icon.
|
|
11
|
+
*
|
|
12
|
+
* @prop {boolean} selected - Whether the tab is selected.
|
|
13
|
+
* @prop {boolean} disabled - Whether the tab is disabled.
|
|
14
|
+
* @prop {string} label - Text label (optional, if not using slot).
|
|
15
|
+
*
|
|
16
|
+
* @fires ds-tab-selected - Dispatched when the tab is clicked/activated.
|
|
17
|
+
*/
|
|
18
|
+
export class DsTabItem extends LitElement {
|
|
19
|
+
static properties = {
|
|
20
|
+
selected: { type: Boolean, reflect: true },
|
|
21
|
+
disabled: { type: Boolean, reflect: true },
|
|
22
|
+
label: { type: String },
|
|
23
|
+
variant: { type: String, reflect: true }, // 'line' | 'contained'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
static styles = css`
|
|
27
|
+
:host {
|
|
28
|
+
display: inline-block;
|
|
29
|
+
outline: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.tab {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
height: 32px;
|
|
38
|
+
/* Padding: Top 6px, Bottom 5px (to accommodate 1px transparent border), Horizontal 16px */
|
|
39
|
+
padding: 6px 16px 5px 16px;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
position: relative;
|
|
42
|
+
background-color: transparent;
|
|
43
|
+
color: var(--ds-color-text-default);
|
|
44
|
+
transition: background-color 0.2s, color 0.2s;
|
|
45
|
+
|
|
46
|
+
/* Transparent border allows parent shadow (divider) to show through */
|
|
47
|
+
border-bottom: 1px solid transparent;
|
|
48
|
+
/* padding-box clip ensures background doesn't cover the transparent border area */
|
|
49
|
+
background-clip: padding-box;
|
|
50
|
+
gap: 4px; /* "4 pixels de distância entre si" */
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
|
|
53
|
+
/* Typography Default */
|
|
54
|
+
font: var(--ds-typo-content-body-regular);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* =========================================
|
|
58
|
+
VARIANT: LINE (Default)
|
|
59
|
+
========================================= */
|
|
60
|
+
/* Shared styles above match Line defaults largely, but we can be explicit if needed */
|
|
61
|
+
|
|
62
|
+
/* =========================================
|
|
63
|
+
VARIANT: LINE (Default)
|
|
64
|
+
========================================= */
|
|
65
|
+
|
|
66
|
+
/* Hover State (Line) */
|
|
67
|
+
:host(:not([variant="contained"])) .tab:hover:not(.disabled) {
|
|
68
|
+
background-color: var(--ds-color-bg-hover);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Selected State (Line) */
|
|
72
|
+
:host([selected]:not([variant="contained"])) .tab {
|
|
73
|
+
font: var(--ds-typo-content-body-bold);
|
|
74
|
+
color: var(--ds-color-text-default);
|
|
75
|
+
}
|
|
76
|
+
:host([selected][disabled]:not([variant="contained"])) .tab {
|
|
77
|
+
/* Use disabled color for text, but keep bold font */
|
|
78
|
+
color: var(--ds-color-text-disabled, inherit);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Indicator (Line) */
|
|
82
|
+
:host(:not([variant="contained"])) .tab::after {
|
|
83
|
+
content: '';
|
|
84
|
+
position: absolute;
|
|
85
|
+
bottom: -1px;
|
|
86
|
+
left: 0;
|
|
87
|
+
width: 100%;
|
|
88
|
+
height: 2px;
|
|
89
|
+
background-color: transparent;
|
|
90
|
+
transition: background-color 0.2s;
|
|
91
|
+
z-index: 2;
|
|
92
|
+
}
|
|
93
|
+
:host([selected]:not([variant="contained"])) .tab::after {
|
|
94
|
+
background-color: var(--ds-color-border-selected);
|
|
95
|
+
}
|
|
96
|
+
/* Disabled Override */
|
|
97
|
+
:host([selected][disabled]:not([variant="contained"])) .tab::after {
|
|
98
|
+
background-color: var(--ds-color-border-disabled, #ccc);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* =========================================
|
|
102
|
+
VARIANT: CONTAINED
|
|
103
|
+
========================================= */
|
|
104
|
+
|
|
105
|
+
:host([variant="contained"]) .tab {
|
|
106
|
+
height: 40px;
|
|
107
|
+
padding: 10px 16px;
|
|
108
|
+
|
|
109
|
+
background-color: var(--ds-color-bg-secondary);
|
|
110
|
+
|
|
111
|
+
border: 1px solid var(--ds-color-border-strong);
|
|
112
|
+
border-bottom-color: transparent;
|
|
113
|
+
|
|
114
|
+
z-index: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Contained Hover */
|
|
118
|
+
:host([variant="contained"]:not([selected])) .tab:hover:not(.disabled) {
|
|
119
|
+
background-color: var(--ds-color-bg-hover);
|
|
120
|
+
z-index: 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Selected State (Contained) */
|
|
124
|
+
:host([variant="contained"][selected]) .tab {
|
|
125
|
+
font: var(--ds-typo-content-body-bold);
|
|
126
|
+
background-color: var(--ds-color-bg-default);
|
|
127
|
+
|
|
128
|
+
border-color: var(--ds-color-border-strong);
|
|
129
|
+
border-top: 2px solid var(--ds-color-border-selected);
|
|
130
|
+
padding-top: 9px;
|
|
131
|
+
|
|
132
|
+
border-bottom: 1px solid var(--ds-color-bg-default);
|
|
133
|
+
|
|
134
|
+
z-index: 2;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Disabled Override for Contained Selected */
|
|
138
|
+
:host([variant="contained"][selected][disabled]) .tab {
|
|
139
|
+
/* Keep white bg? Or maybe grey? Usually keep structure but grey text/border */
|
|
140
|
+
color: var(--ds-color-text-disabled);
|
|
141
|
+
border-top-color: var(--ds-color-border-disabled);
|
|
142
|
+
/* Ensure bottom border matches BG (if BG changes, this must change) */
|
|
143
|
+
/* If disabled selected tab has same BG as panel, let's assume default white. */
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Remove ::after indicator for Contained */
|
|
147
|
+
:host([variant="contained"]) .tab::after {
|
|
148
|
+
content: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Focus State (Shared Logic) */
|
|
152
|
+
:host(:focus-visible) {
|
|
153
|
+
z-index: 3;
|
|
154
|
+
position: relative;
|
|
155
|
+
}
|
|
156
|
+
:host(:focus-visible) .tab {
|
|
157
|
+
/* User requested focus "more outside" and "hiding lines".
|
|
158
|
+
outline: 2px solid with offset -2px positions the outline
|
|
159
|
+
inside the border-box edge.
|
|
160
|
+
If border is 1px, this 2px outline covers the 1px border + 1px padding.
|
|
161
|
+
It effectively paints ON TOP of the border.
|
|
162
|
+
*/
|
|
163
|
+
outline: 2px solid var(--ds-color-border-focus);
|
|
164
|
+
outline-offset: -2px;
|
|
165
|
+
box-shadow: none; /* Remove previous inset shadow strategy */
|
|
166
|
+
}
|
|
167
|
+
/*
|
|
168
|
+
Problem: User said "focus não aparece outline".
|
|
169
|
+
On a filled background (bg-secondary), inset shadow works.
|
|
170
|
+
Maybe z-index 3 is not enough?
|
|
171
|
+
Or maybe 'outline' property is being overridden?
|
|
172
|
+
We use 'outline: 2px solid transparent' which is correct for a11y.
|
|
173
|
+
The visual is 'box-shadow'.
|
|
174
|
+
If the tab has borders, box-shadow inset paints *inside* the borders.
|
|
175
|
+
Confirmed this logic usually works.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
/* Disabled State */
|
|
179
|
+
.tab.disabled {
|
|
180
|
+
cursor: not-allowed;
|
|
181
|
+
opacity: var(--ds-opacity-disabled, 0.5);
|
|
182
|
+
pointer-events: none;
|
|
183
|
+
}
|
|
184
|
+
:host([disabled]) {
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ... Layout Shift logic remains ... */
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
Layout Shift Prevention for Bold Text
|
|
192
|
+
We use a pseudo-element on the label container to reserve space
|
|
193
|
+
for the bold version of the text.
|
|
194
|
+
*/
|
|
195
|
+
.label-container {
|
|
196
|
+
display: inline-flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
align-items: center;
|
|
199
|
+
position: relative;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Use attr(data-text) to duplicate content */
|
|
203
|
+
.label-container::before {
|
|
204
|
+
content: attr(data-text);
|
|
205
|
+
font: var(--ds-typo-content-body-bold);
|
|
206
|
+
height: 0;
|
|
207
|
+
visibility: hidden;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
/* This ensures the container preserves the width of the bold text */
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.label-text {
|
|
213
|
+
/* Position absolute? No, just let it sit there.
|
|
214
|
+
Wait, if ::before reserves width, the actual text needs to overlap it
|
|
215
|
+
or just be centered?
|
|
216
|
+
Method: Grid or Flex overlay.
|
|
217
|
+
*/
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Improved Layout Shift Prevention: CSS Grid Override */
|
|
221
|
+
.label-container {
|
|
222
|
+
display: inline-grid;
|
|
223
|
+
grid-template-columns: 1fr;
|
|
224
|
+
justify-items: center;
|
|
225
|
+
align-items: center;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.label-container::before {
|
|
229
|
+
grid-area: 1 / 1;
|
|
230
|
+
content: attr(data-text);
|
|
231
|
+
font: var(--ds-typo-content-body-bold);
|
|
232
|
+
visibility: hidden;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.label-text {
|
|
236
|
+
grid-area: 1 / 1;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
::slotted(ds-icon) {
|
|
241
|
+
--size: var(--ds-icon-size-sm, 20px);
|
|
242
|
+
color: var(--ds-color-icon-default);
|
|
243
|
+
display: flex;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ds-icon {
|
|
247
|
+
display: flex;
|
|
248
|
+
--size: var(--ds-icon-size-sm, 20px);
|
|
249
|
+
color: var(--ds-color-icon-default);
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
constructor() {
|
|
254
|
+
super();
|
|
255
|
+
this.selected = false;
|
|
256
|
+
this.disabled = false;
|
|
257
|
+
this.label = '';
|
|
258
|
+
this.variant = 'line';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Accessibility properties */
|
|
262
|
+
connectedCallback() {
|
|
263
|
+
super.connectedCallback();
|
|
264
|
+
this.setAttribute('role', 'tab');
|
|
265
|
+
this.addEventListener('click', this._handleClick);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
disconnectedCallback() {
|
|
269
|
+
super.disconnectedCallback();
|
|
270
|
+
this.removeEventListener('click', this._handleClick);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
updated(changedProperties) {
|
|
274
|
+
if (changedProperties.has('selected')) {
|
|
275
|
+
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
|
|
276
|
+
this.setAttribute('tabindex', this.selected ? '0' : '-1');
|
|
277
|
+
}
|
|
278
|
+
if (changedProperties.has('disabled')) {
|
|
279
|
+
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
|
280
|
+
if (this.disabled) {
|
|
281
|
+
this.removeAttribute('tabindex');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_handleClick(e) {
|
|
287
|
+
if (this.disabled) {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
e.stopImmediatePropagation();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.dispatchEvent(new CustomEvent('ds-tab-selected', {
|
|
293
|
+
bubbles: true,
|
|
294
|
+
composed: true
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_handleSlotChange(e) {
|
|
299
|
+
this.requestUpdate();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
render() {
|
|
303
|
+
const labelContent = this.label ||
|
|
304
|
+
(this.shadowRoot ? this.shadowRoot.querySelector('slot:not([name])')?.assignedNodes({ flatten: true }).map(n => n.textContent).join('').trim() : '');
|
|
305
|
+
|
|
306
|
+
// We need the text content for the layout shift trick
|
|
307
|
+
// If usage is <ds-tab>Label</ds-tab>, we need to extract "Label".
|
|
308
|
+
// This is tricky with Shadow DOM slots.
|
|
309
|
+
// Easier if we wrap the slot content in the grid trick.
|
|
310
|
+
// But we can't easily get 'data-text' from a slot content dynamically in CSS without JS.
|
|
311
|
+
// Compromise: Implementation relies on 'label' prop for the trick,
|
|
312
|
+
// OR we just accept that if you use slot, you might get shift,
|
|
313
|
+
// unless we use a ResizeObserver logic which is too heavy.
|
|
314
|
+
// BETTER: Just apply the bold font to the slot wrapper? No, that causes shift.
|
|
315
|
+
|
|
316
|
+
// Let's assume 'label' prop is preferred for this optimization,
|
|
317
|
+
// but we can try to grab textContent if available.
|
|
318
|
+
|
|
319
|
+
// For now, I will implement the logic primarily around `label`.
|
|
320
|
+
// If the USER uses slots for complex content, they might accept the shift or we refine later.
|
|
321
|
+
|
|
322
|
+
return html`
|
|
323
|
+
<div
|
|
324
|
+
class="tab ${this.disabled ? 'disabled' : ''}"
|
|
325
|
+
>
|
|
326
|
+
<slot name="icon"></slot>
|
|
327
|
+
|
|
328
|
+
<span class="label-container" data-text=${ifDefined(this.label)}>
|
|
329
|
+
<!-- Reserve space using pseudo element on container -->
|
|
330
|
+
<span class="label-text">
|
|
331
|
+
${this.label ? this.label : html`<slot></slot>`}
|
|
332
|
+
</span>
|
|
333
|
+
</span>
|
|
334
|
+
</div>
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!customElements.get('ds-tab-item')) {
|
|
340
|
+
customElements.define('ds-tab-item', DsTabItem);
|
|
341
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './ds-tab-item.js';
|
|
3
|
+
import '../ds-icon/ds-icon.js';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Tab Item',
|
|
7
|
+
component: 'ds-tab-item',
|
|
8
|
+
argTypes: {
|
|
9
|
+
label: { control: 'text' },
|
|
10
|
+
selected: { control: 'boolean' },
|
|
11
|
+
disabled: { control: 'boolean' },
|
|
12
|
+
variant: {
|
|
13
|
+
control: { type: 'select' },
|
|
14
|
+
options: ['line', 'contained'],
|
|
15
|
+
defaultValue: 'line'
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const Template = (args) => html`
|
|
21
|
+
<ds-tab-item
|
|
22
|
+
label=${args.label}
|
|
23
|
+
?selected=${args.selected}
|
|
24
|
+
?disabled=${args.disabled}
|
|
25
|
+
variant=${args.variant || 'line'}
|
|
26
|
+
>
|
|
27
|
+
${args.icon ? html`<ds-icon slot="icon" name=${args.icon}></ds-icon>` : ''}
|
|
28
|
+
</ds-tab-item>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const Default = Template.bind({});
|
|
32
|
+
Default.args = {
|
|
33
|
+
label: 'Single Tab',
|
|
34
|
+
selected: false,
|
|
35
|
+
disabled: false,
|
|
36
|
+
variant: 'line',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Selected = Template.bind({});
|
|
40
|
+
Selected.args = {
|
|
41
|
+
label: 'Selected Tab',
|
|
42
|
+
selected: true,
|
|
43
|
+
disabled: false,
|
|
44
|
+
variant: 'line',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Disabled = Template.bind({});
|
|
48
|
+
Disabled.args = {
|
|
49
|
+
label: 'Disabled Tab',
|
|
50
|
+
selected: false,
|
|
51
|
+
disabled: true,
|
|
52
|
+
variant: 'line',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Contained = Template.bind({});
|
|
56
|
+
Contained.args = {
|
|
57
|
+
label: 'Contained Tab',
|
|
58
|
+
selected: false,
|
|
59
|
+
disabled: false,
|
|
60
|
+
variant: 'contained',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const WithIcon = Template.bind({});
|
|
64
|
+
WithIcon.args = {
|
|
65
|
+
label: 'Home',
|
|
66
|
+
icon: 'home',
|
|
67
|
+
selected: false,
|
|
68
|
+
variant: 'line',
|
|
69
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @element ds-tab-panel
|
|
5
|
+
* @summary A panel containing content associated with a tab.
|
|
6
|
+
*
|
|
7
|
+
* @slot - Default slot for panel content.
|
|
8
|
+
*
|
|
9
|
+
* @prop {boolean} selected - Whether the panel is active/visible.
|
|
10
|
+
*/
|
|
11
|
+
export class DsTabPanel extends LitElement {
|
|
12
|
+
static properties = {
|
|
13
|
+
selected: { type: Boolean, reflect: true },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
static styles = css`
|
|
17
|
+
:host {
|
|
18
|
+
display: none;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
/* Default styles for panel content - can be overridden */
|
|
21
|
+
padding: var(--ds-space-md, 16px);
|
|
22
|
+
color: var(--ds-color-text-default);
|
|
23
|
+
font: var(--ds-typo-content-body-regular);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host([selected]) {
|
|
27
|
+
display: block;
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
this.selected = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
connectedCallback() {
|
|
37
|
+
super.connectedCallback();
|
|
38
|
+
this.setAttribute('role', 'tabpanel');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render() {
|
|
42
|
+
return html`<slot></slot>`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!customElements.get('ds-tab-panel')) {
|
|
47
|
+
customElements.define('ds-tab-panel', DsTabPanel);
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import './ds-tabs.js';
|
|
3
|
+
import '../ds-tab-item/ds-tab-item.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-tabs a11y', () => {
|
|
6
|
+
let container;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
container = document.createElement('div');
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
container.remove();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('ds-tabs has role tablist', async () => {
|
|
18
|
+
container.innerHTML = '<ds-tabs></ds-tabs>';
|
|
19
|
+
const el = container.querySelector('ds-tabs');
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
21
|
+
expect(el.getAttribute('role')).toBe('tablist');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('ds-tab-item has role tab', async () => {
|
|
25
|
+
container.innerHTML = '<ds-tab-item label="Tab"></ds-tab-item>';
|
|
26
|
+
const el = container.querySelector('ds-tab-item');
|
|
27
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
28
|
+
expect(el.getAttribute('role')).toBe('tab');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('selected tab has aria-selected=true and tabindex=0', async () => {
|
|
32
|
+
container.innerHTML = '<ds-tab-item label="Tab" selected></ds-tab-item>';
|
|
33
|
+
const el = container.querySelector('ds-tab-item');
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
35
|
+
expect(el.getAttribute('aria-selected')).toBe('true');
|
|
36
|
+
expect(el.getAttribute('tabindex')).toBe('0');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('unselected tab has aria-selected=false and tabindex=-1', async () => {
|
|
40
|
+
container.innerHTML = '<ds-tab-item label="Tab"></ds-tab-item>';
|
|
41
|
+
const el = container.querySelector('ds-tab-item');
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
43
|
+
expect(el.getAttribute('aria-selected')).toBe('false');
|
|
44
|
+
expect(el.getAttribute('tabindex')).toBe('-1');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('disabled tab has aria-disabled=true', async () => {
|
|
48
|
+
container.innerHTML = '<ds-tab-item label="Tab" disabled></ds-tab-item>';
|
|
49
|
+
const el = container.querySelector('ds-tab-item');
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
51
|
+
expect(el.getAttribute('aria-disabled')).toBe('true');
|
|
52
|
+
// Disabled elements should not be focusable usually, or -1.
|
|
53
|
+
// ds-tab implementation removes tabindex.
|
|
54
|
+
expect(el.hasAttribute('tabindex')).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// ds-tabs.js
|
|
2
|
+
import { LitElement, html, css } from 'lit';
|
|
3
|
+
import '../ds-tab-item/ds-tab-item.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @element ds-tabs
|
|
7
|
+
* @summary A container for tab items that manages selection and keyboard navigation.
|
|
8
|
+
*
|
|
9
|
+
* @slot - Default slot for ds-tab-item items.
|
|
10
|
+
*
|
|
11
|
+
* @prop {string} variant - Visual variant ('line' | 'contained'). Default: 'line'.
|
|
12
|
+
*/
|
|
13
|
+
export class DsTabs extends LitElement {
|
|
14
|
+
static properties = {
|
|
15
|
+
variant: { type: String, reflect: true },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
static styles = css`
|
|
19
|
+
:host {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: row;
|
|
22
|
+
align-items: center;
|
|
23
|
+
width: 100%;
|
|
24
|
+
|
|
25
|
+
/* Optional: Scrollable if content overflows */
|
|
26
|
+
overflow-x: auto;
|
|
27
|
+
scrollbar-width: none; /* Firefox */
|
|
28
|
+
-ms-overflow-style: none; /* IE/Edge */
|
|
29
|
+
|
|
30
|
+
/* Padding removed as we use inner outline for focus now */
|
|
31
|
+
padding: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Line Variant (Default) Specifics */
|
|
35
|
+
:host([variant="line"]) {
|
|
36
|
+
box-shadow: inset 0 -1px 0 var(--ds-color-border-strong);
|
|
37
|
+
gap: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Contained Variant Specifics */
|
|
41
|
+
:host([variant="contained"]) {
|
|
42
|
+
gap: 0;
|
|
43
|
+
padding: 0;
|
|
44
|
+
|
|
45
|
+
/* Bottom Track (Inset Shadow) */
|
|
46
|
+
box-shadow: inset 0 -1px 0 var(--ds-color-border-strong);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Margin Overlap Strategy */
|
|
50
|
+
:host([variant="contained"]) ::slotted(ds-tab-item:not(:first-child)) {
|
|
51
|
+
margin-left: -1px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:host::-webkit-scrollbar {
|
|
55
|
+
display: none; /* Chrome/Safari */
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
constructor() {
|
|
60
|
+
super();
|
|
61
|
+
this.variant = 'line';
|
|
62
|
+
this._handleTabSelected = this._handleTabSelected.bind(this);
|
|
63
|
+
this._handleKeydown = this._handleKeydown.bind(this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
super.connectedCallback();
|
|
68
|
+
this.addEventListener('ds-tab-selected', this._handleTabSelected);
|
|
69
|
+
this.addEventListener('keydown', this._handleKeydown);
|
|
70
|
+
this.setAttribute('role', 'tablist');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
disconnectedCallback() {
|
|
74
|
+
super.disconnectedCallback();
|
|
75
|
+
this.removeEventListener('ds-tab-selected', this._handleTabSelected);
|
|
76
|
+
this.removeEventListener('keydown', this._handleKeydown);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
updated(changedProperties) {
|
|
80
|
+
if (changedProperties.has('variant')) {
|
|
81
|
+
this._propagateVariant();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
firstUpdated() {
|
|
86
|
+
this._propagateVariant();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_handleSlotChange(e) {
|
|
90
|
+
this._propagateVariant();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_propagateVariant() {
|
|
94
|
+
const tabs = [...this.querySelectorAll('ds-tab-item')];
|
|
95
|
+
tabs.forEach(tab => {
|
|
96
|
+
if (tab.variant !== this.variant) {
|
|
97
|
+
tab.variant = this.variant;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handles selection events from child tabs.
|
|
104
|
+
* Updates state to ensure only one tab is selected.
|
|
105
|
+
*/
|
|
106
|
+
_handleTabSelected(event) {
|
|
107
|
+
const selectedTab = event.target;
|
|
108
|
+
this._activateTab(selectedTab);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_activateTab(selectedTab) {
|
|
112
|
+
const tabs = [...this.querySelectorAll('ds-tab-item')];
|
|
113
|
+
tabs.forEach(tab => {
|
|
114
|
+
const isSelected = tab === selectedTab;
|
|
115
|
+
if (tab.selected !== isSelected) {
|
|
116
|
+
tab.selected = isSelected;
|
|
117
|
+
}
|
|
118
|
+
// ARIA management could happen here or in the item itself reflecting props
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handles keyboard navigation (Arrow keys).
|
|
124
|
+
*/
|
|
125
|
+
_handleKeydown(e) {
|
|
126
|
+
const tabs = [...this.querySelectorAll('ds-tab-item')].filter(t => !t.disabled);
|
|
127
|
+
if (!tabs.length) return;
|
|
128
|
+
|
|
129
|
+
const currentTab = e.target;
|
|
130
|
+
const currentIndex = tabs.indexOf(currentTab);
|
|
131
|
+
|
|
132
|
+
// Only handle if focus is essentially inside a tab
|
|
133
|
+
if (currentIndex === -1) return;
|
|
134
|
+
|
|
135
|
+
let nextIndex;
|
|
136
|
+
|
|
137
|
+
switch (e.key) {
|
|
138
|
+
case 'ArrowRight':
|
|
139
|
+
nextIndex = (currentIndex + 1) % tabs.length;
|
|
140
|
+
break;
|
|
141
|
+
case 'ArrowLeft':
|
|
142
|
+
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
143
|
+
break;
|
|
144
|
+
case 'Home':
|
|
145
|
+
nextIndex = 0;
|
|
146
|
+
break;
|
|
147
|
+
case 'End':
|
|
148
|
+
nextIndex = tabs.length - 1;
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
const nextTab = tabs[nextIndex];
|
|
156
|
+
if (nextTab) {
|
|
157
|
+
nextTab.focus();
|
|
158
|
+
// Optional: Automatic selection on focus (common for tabs)
|
|
159
|
+
// For now, we'll stick to focus, user presses Enter/Space to select -> ds-tab handles click/enter
|
|
160
|
+
// Actually, standard aria practices often suggest automatic activation for tabs.
|
|
161
|
+
// But let's check requirements. "Implementation Plan" didn't specify auto-follow.
|
|
162
|
+
// Let's stick to standard behavior: arrow moves focus. User activates.
|
|
163
|
+
// Wait, standard WAI-ARIA Authoring Practices 1.2:
|
|
164
|
+
// "It is recommended that tabs activate automatically when they receive focus,
|
|
165
|
+
// as long as their associated tab panels are displayed without significant latency."
|
|
166
|
+
// I will trigger click() to activate it.
|
|
167
|
+
nextTab.click();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
render() {
|
|
172
|
+
return html`
|
|
173
|
+
<slot @slotchange=${this._handleSlotChange}></slot>
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!customElements.get('ds-tabs')) {
|
|
179
|
+
customElements.define('ds-tabs', DsTabs);
|
|
180
|
+
}
|