@dile/ui 3.18.2 → 3.18.4
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/components/card/src/DileCardSlide.js +10 -1
- package/components/menu-overlay/src/DileMenuOverlay.js +50 -0
- package/components/selector/src/DileSelectorItem.js +65 -20
- package/components/selector/src/DileSelectorOverlay.js +92 -5
- package/components/tabs/src/DileTab.js +70 -16
- package/components/tabs/src/DileTabs.js +63 -0
- package/package.json +2 -2
|
@@ -3,16 +3,18 @@ import { DileCard } from './DileCard.js';
|
|
|
3
3
|
import { DileSlideDown } from '../../../mixins/slide-down/index.js';
|
|
4
4
|
import { arrowDropDownIcon } from '@dile/icons';
|
|
5
5
|
import '@dile/ui/components/icon/icon.js';
|
|
6
|
+
import '@dile/iconlib/dile-iconlib.js';
|
|
6
7
|
|
|
7
8
|
export class DileCardSlide extends DileSlideDown(DileCard) {
|
|
8
9
|
static get properties() {
|
|
9
10
|
return {
|
|
10
11
|
opened: { type: Boolean },
|
|
11
12
|
icon: { type: Object },
|
|
13
|
+
decorativeIcon: { type: String },
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
static get styles() {
|
|
16
18
|
return [
|
|
17
19
|
...super.styles,
|
|
18
20
|
css`
|
|
@@ -41,6 +43,12 @@ export class DileCardSlide extends DileSlideDown(DileCard) {
|
|
|
41
43
|
transition: height 0.5s ease-in;
|
|
42
44
|
-webkit-transition: height 0.5s ease-in;
|
|
43
45
|
}
|
|
46
|
+
.decorative-icon {
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
margin-right: 0.5rem;
|
|
49
|
+
--dile-icon-size: var(--dile-card-slide-decorative-icon-size, 24px);
|
|
50
|
+
--dile-icon-color: var(--dile-card-slide-decorative-icon-color, var(--dile-primary-dark-color, #303030));
|
|
51
|
+
}
|
|
44
52
|
dile-icon {
|
|
45
53
|
flex-shrink: 0;
|
|
46
54
|
transform: rotate(-90deg);
|
|
@@ -93,6 +101,7 @@ export class DileCardSlide extends DileSlideDown(DileCard) {
|
|
|
93
101
|
? html`
|
|
94
102
|
<div class="card-title" role="heading" aria-level="${this.titleLevel}">
|
|
95
103
|
<button @click="${this.toggle}" class="${this.opened ? 'opened' : ''}" aria-expanded="${this.opened ? 'true' : 'false'}" aria-controls="${this._contentId}">
|
|
104
|
+
${this.decorativeIcon ? html`<dile-iconlib class="decorative-icon" icon=${this.decorativeIcon} aria-hidden="true"></dile-iconlib>` : ''}
|
|
96
105
|
<span>${this.title}</span>
|
|
97
106
|
<dile-icon .icon=${this.icon}></dile-icon>
|
|
98
107
|
</button>
|
|
@@ -46,6 +46,28 @@ export class DileMenuOverlay extends DileOverlay(DileCloseDocumentClick(LitEleme
|
|
|
46
46
|
constructor() {
|
|
47
47
|
super();
|
|
48
48
|
this.closeOnClickInside = false;
|
|
49
|
+
this._escKeydownHandler = this._onEscKeydown.bind(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
connectedCallback() {
|
|
53
|
+
super.connectedCallback();
|
|
54
|
+
// In the capture phase, so an opened menu gets the key before the layers that contain it, like a modal.
|
|
55
|
+
window.addEventListener('keydown', this._escKeydownHandler, true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
disconnectedCallback() {
|
|
59
|
+
super.disconnectedCallback();
|
|
60
|
+
window.removeEventListener('keydown', this._escKeydownHandler, true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
updated(changedProperties) {
|
|
64
|
+
super.updated(changedProperties);
|
|
65
|
+
if (changedProperties.has('_overlayClass') && this._overlayClass === 'opened') {
|
|
66
|
+
this.dispatchEvent(new CustomEvent('overlay-ready', {
|
|
67
|
+
bubbles: true,
|
|
68
|
+
composed: true,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
render() {
|
|
@@ -56,6 +78,7 @@ export class DileMenuOverlay extends DileOverlay(DileCloseDocumentClick(LitEleme
|
|
|
56
78
|
<div
|
|
57
79
|
id="overlay"
|
|
58
80
|
class="${this._overlayClass}"
|
|
81
|
+
?inert="${!this._overlayClass && !this._opening}"
|
|
59
82
|
@click="${this.clickHandler}"
|
|
60
83
|
>
|
|
61
84
|
<slot name="content"></slot>
|
|
@@ -63,6 +86,33 @@ export class DileMenuOverlay extends DileOverlay(DileCloseDocumentClick(LitEleme
|
|
|
63
86
|
`;
|
|
64
87
|
}
|
|
65
88
|
|
|
89
|
+
open() {
|
|
90
|
+
if (!this._overlayClass && !this._opening) {
|
|
91
|
+
this._focusBeforeOpen = this._getActiveElement();
|
|
92
|
+
}
|
|
93
|
+
super.open();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_getActiveElement() {
|
|
97
|
+
let active = document.activeElement;
|
|
98
|
+
while (active?.shadowRoot?.activeElement) {
|
|
99
|
+
active = active.shadowRoot.activeElement;
|
|
100
|
+
}
|
|
101
|
+
return active;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_onEscKeydown(e) {
|
|
105
|
+
if (e.key !== 'Escape' || this._overlayClass !== 'opened') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Only the menu closes: a modal or drawer that contains it stays opened until the next Escape.
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
this.close();
|
|
111
|
+
if (this.overlay.matches(':focus-within') && this._focusBeforeOpen?.isConnected) {
|
|
112
|
+
this._focusBeforeOpen.focus();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
66
116
|
clickHandler(e) {
|
|
67
117
|
if(! this.closeOnClickInside) {
|
|
68
118
|
e.stopPropagation();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { html, css, LitElement } from "lit";
|
|
2
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
2
3
|
import { DileSelectableItem } from "../../../mixins/selectable/index.js";
|
|
3
4
|
import '@dile/iconlib/dile-iconlib.js';
|
|
4
5
|
|
|
@@ -21,7 +22,19 @@ export class DileSelectorItem extends DileSelectableItem(LitElement) {
|
|
|
21
22
|
display: block;
|
|
22
23
|
cursor: pointer;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
.item {
|
|
26
|
+
appearance: none;
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
width: 100%;
|
|
29
|
+
margin: 0;
|
|
30
|
+
border: 0;
|
|
31
|
+
border-radius: 0;
|
|
32
|
+
font: inherit;
|
|
33
|
+
letter-spacing: inherit;
|
|
34
|
+
word-spacing: inherit;
|
|
35
|
+
text-align: inherit;
|
|
36
|
+
text-transform: inherit;
|
|
37
|
+
cursor: inherit;
|
|
25
38
|
display: flex;
|
|
26
39
|
align-items: center;
|
|
27
40
|
padding-top: var(--dile-selector-padding-y, 0.2rem);
|
|
@@ -32,6 +45,11 @@ export class DileSelectorItem extends DileSelectableItem(LitElement) {
|
|
|
32
45
|
color: var(--dile-selector-text-color, var(--dile-on-background-color, #303030));
|
|
33
46
|
transition: all 0.3s ease-in;
|
|
34
47
|
}
|
|
48
|
+
.item:focus-visible {
|
|
49
|
+
outline: var(--dile-selector-focus-ring-width, 2px) solid var(--dile-selector-focus-ring-color, currentColor);
|
|
50
|
+
outline-offset: var(--dile-selector-focus-ring-offset, -2px);
|
|
51
|
+
transition-property: background-color, color;
|
|
52
|
+
}
|
|
35
53
|
.selected {
|
|
36
54
|
background-color: var(--dile-selector-selected-background-color, #039be5);
|
|
37
55
|
color: var(--dile-selector-selected-text-color, #fff);
|
|
@@ -59,8 +77,7 @@ export class DileSelectorItem extends DileSelectableItem(LitElement) {
|
|
|
59
77
|
.selected .icon dile-iconlib {
|
|
60
78
|
--dile-icon-color: var(--dile-selector-selected-icon-color, #fff);
|
|
61
79
|
}
|
|
62
|
-
a {
|
|
63
|
-
display: block;
|
|
80
|
+
a.item {
|
|
64
81
|
text-decoration: var(--dile-selector-text-decoration, none);
|
|
65
82
|
}
|
|
66
83
|
`;
|
|
@@ -71,6 +88,8 @@ export class DileSelectorItem extends DileSelectableItem(LitElement) {
|
|
|
71
88
|
icon: { type: String },
|
|
72
89
|
iconlibIcon: { type: String },
|
|
73
90
|
href: { type: String },
|
|
91
|
+
/** ARIA role of the inner control. dile-selector-overlay sets it to "menuitem" */
|
|
92
|
+
itemRole: { type: String },
|
|
74
93
|
};
|
|
75
94
|
}
|
|
76
95
|
|
|
@@ -80,29 +99,55 @@ export class DileSelectorItem extends DileSelectableItem(LitElement) {
|
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
render() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
const itemClass = `item ${this.selected ? 'selected' : ''}`;
|
|
103
|
+
const ariaCurrent = ifDefined(this.selected ? 'true' : undefined);
|
|
104
|
+
const role = ifDefined(this.itemRole || undefined);
|
|
105
|
+
return this.href == ''
|
|
106
|
+
? html`<button
|
|
107
|
+
type="button"
|
|
108
|
+
class="${itemClass}"
|
|
109
|
+
role="${role}"
|
|
110
|
+
aria-current="${ariaCurrent}"
|
|
111
|
+
@click="${this.select}"
|
|
112
|
+
>${this.itemTemplate}</button>`
|
|
113
|
+
: html`<a
|
|
114
|
+
href="${this.href}"
|
|
115
|
+
class="${itemClass}"
|
|
116
|
+
role="${role}"
|
|
117
|
+
aria-current="${ariaCurrent}"
|
|
118
|
+
@click="${this.select}"
|
|
119
|
+
@keydown="${this._onLinkKeydown}"
|
|
120
|
+
>${this.itemTemplate}</a>`;
|
|
89
121
|
}
|
|
90
122
|
|
|
91
123
|
get itemTemplate() {
|
|
92
124
|
return html`
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<div class="line"><slot></slot></div>
|
|
103
|
-
</article>
|
|
125
|
+
${this.icon
|
|
126
|
+
? html`<span class="icon">${this.iconElement(this.icon)}</span>`
|
|
127
|
+
: ''
|
|
128
|
+
}
|
|
129
|
+
${this.iconlibIcon
|
|
130
|
+
? html`<span class="icon">${this.renderIconlibIcon(this.iconlibIcon)}</span>`
|
|
131
|
+
: ''
|
|
132
|
+
}
|
|
133
|
+
<span class="line"><slot></slot></span>
|
|
104
134
|
`;
|
|
105
135
|
}
|
|
136
|
+
|
|
137
|
+
// A native link is only activated with Enter
|
|
138
|
+
_onLinkKeydown(e) {
|
|
139
|
+
if (e.key === ' ') {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
if (!e.repeat) {
|
|
142
|
+
e.currentTarget.click();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
focus(options) {
|
|
148
|
+
this.shadowRoot?.querySelector('.item')?.focus(options);
|
|
149
|
+
}
|
|
150
|
+
|
|
106
151
|
iconElement(icon) {
|
|
107
152
|
return icons[icon];
|
|
108
153
|
}
|
|
@@ -21,7 +21,11 @@ export class DileSelectorOverlay extends DileSelectable(LitElement) {
|
|
|
21
21
|
|
|
22
22
|
_displayLabel: { state: true },
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
_menuOpened: { state: true },
|
|
25
|
+
|
|
26
|
+
/** Accessible text saying that the trigger opens a menu of options. It's the whole accessible name
|
|
27
|
+
* when there is no visible label text, and it follows the label text when there is one.
|
|
28
|
+
* Set it to translate it, or to an empty string to expose only the label text. */
|
|
25
29
|
triggerAriaLabel: { type: String },
|
|
26
30
|
|
|
27
31
|
/** Forwarded to the internal dile-menu-overlay */
|
|
@@ -56,6 +60,7 @@ export class DileSelectorOverlay extends DileSelectable(LitElement) {
|
|
|
56
60
|
this.dynamicLabel = false;
|
|
57
61
|
this.icon = 'ellipsis-vertical';
|
|
58
62
|
this._displayLabel = null;
|
|
63
|
+
this._menuOpened = false;
|
|
59
64
|
this.triggerAriaLabel = 'Options';
|
|
60
65
|
this.horizontalAlign = 'under_left';
|
|
61
66
|
this.verticalAlign = 'bottom';
|
|
@@ -71,6 +76,9 @@ export class DileSelectorOverlay extends DileSelectable(LitElement) {
|
|
|
71
76
|
moveTop="${this.moveTop}"
|
|
72
77
|
moveLeft="${this.moveLeft}"
|
|
73
78
|
closeOnClickInside
|
|
79
|
+
@overlay-opened="${this._onOverlayOpened}"
|
|
80
|
+
@overlay-ready="${this._onOverlayReady}"
|
|
81
|
+
@overlay-closed="${this._onOverlayClosed}"
|
|
74
82
|
>
|
|
75
83
|
<span
|
|
76
84
|
slot="trigger"
|
|
@@ -78,7 +86,8 @@ export class DileSelectorOverlay extends DileSelectable(LitElement) {
|
|
|
78
86
|
role="button"
|
|
79
87
|
tabindex="0"
|
|
80
88
|
aria-haspopup="true"
|
|
81
|
-
aria-
|
|
89
|
+
aria-expanded="${this._menuOpened ? 'true' : 'false'}"
|
|
90
|
+
aria-label="${this._triggerAccessibleName}"
|
|
82
91
|
@keydown="${this._onTriggerKeydown}"
|
|
83
92
|
>
|
|
84
93
|
${this._displayLabel
|
|
@@ -86,8 +95,8 @@ export class DileSelectorOverlay extends DileSelectable(LitElement) {
|
|
|
86
95
|
: html`<dile-lucide-icon icon="${this.icon}" rounded></dile-lucide-icon>`
|
|
87
96
|
}
|
|
88
97
|
</span>
|
|
89
|
-
<div slot="content">
|
|
90
|
-
<slot></slot>
|
|
98
|
+
<div slot="content" role="menu" aria-labelledby="trigger" @keydown="${this._onMenuKeydown}">
|
|
99
|
+
<slot @slotchange="${this._onSlotChange}"></slot>
|
|
91
100
|
</div>
|
|
92
101
|
</dile-menu-overlay>
|
|
93
102
|
`;
|
|
@@ -124,10 +133,88 @@ export class DileSelectorOverlay extends DileSelectable(LitElement) {
|
|
|
124
133
|
this._displayLabel = selectedItem ? selectedItem.textContent.trim() : this.label;
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
get _triggerAccessibleName() {
|
|
137
|
+
if (!this._displayLabel) {
|
|
138
|
+
return this.triggerAriaLabel;
|
|
139
|
+
}
|
|
140
|
+
return !this.triggerAriaLabel || this.triggerAriaLabel === this._displayLabel
|
|
141
|
+
? this._displayLabel
|
|
142
|
+
: `${this._displayLabel}, ${this.triggerAriaLabel}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get _menu() {
|
|
146
|
+
return this.shadowRoot.querySelector('dile-menu-overlay');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Children that aren't options (e.g. a separator) and hidden options are skipped.
|
|
150
|
+
get _focusableItems() {
|
|
151
|
+
return this.getItems().filter((item) => 'selected' in item && item.getClientRects().length > 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_onSlotChange() {
|
|
155
|
+
this.getItems().forEach((item) => {
|
|
156
|
+
item.itemRole = 'menuitem';
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_onOverlayOpened(e) {
|
|
161
|
+
if (e.target === e.currentTarget) {
|
|
162
|
+
this._menuOpened = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_onOverlayReady(e) {
|
|
167
|
+
if (e.target === e.currentTarget) {
|
|
168
|
+
const items = this._focusableItems;
|
|
169
|
+
(items.find((item) => item.selected) ?? items[0])?.focus();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_onOverlayClosed(e) {
|
|
174
|
+
if (e.target !== e.currentTarget) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this._menuOpened = false;
|
|
178
|
+
// When the menu closes by clicking elsewhere, the focus is already out of here and has to stay there.
|
|
179
|
+
if (this.matches(':focus-within')) {
|
|
180
|
+
this.shadowRoot.getElementById('trigger').focus();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
127
184
|
_onTriggerKeydown(e) {
|
|
128
185
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
129
186
|
e.preventDefault();
|
|
130
|
-
e.
|
|
187
|
+
if (!e.repeat) {
|
|
188
|
+
e.currentTarget.click();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_onMenuKeydown(e) {
|
|
194
|
+
if (e.key === 'Tab') {
|
|
195
|
+
// The focus goes back to the trigger, so the browser moves it from there to the next element of the page.
|
|
196
|
+
this._menu.close();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (e.altKey || e.ctrlKey || e.metaKey) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.repeat) {
|
|
203
|
+
// Keeping the key pressed on the trigger must not activate the option that received the focus.
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const items = this._focusableItems;
|
|
208
|
+
const current = items.findIndex((item) => item.matches(':focus-within'));
|
|
209
|
+
const nextIndex = {
|
|
210
|
+
ArrowDown: (current + 1) % items.length,
|
|
211
|
+
ArrowUp: (current - 1 + items.length) % items.length,
|
|
212
|
+
Home: 0,
|
|
213
|
+
End: items.length - 1,
|
|
214
|
+
}[e.key];
|
|
215
|
+
if (items[nextIndex]) {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
items[nextIndex].focus();
|
|
131
218
|
}
|
|
132
219
|
}
|
|
133
220
|
}
|
|
@@ -8,16 +8,36 @@ export class DileTab extends DileSelectableItem(LitElement) {
|
|
|
8
8
|
return {
|
|
9
9
|
href: { type: String },
|
|
10
10
|
target: { type: String },
|
|
11
|
+
/** If this tab is in the page tab sequence. dile-tabs keeps it true only on one of its tabs */
|
|
12
|
+
tabbable: { type: Boolean, attribute: false },
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.tabbable = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
static get styles() {
|
|
15
22
|
return css`
|
|
16
23
|
:host {
|
|
17
24
|
display: inline-block;
|
|
18
25
|
margin: 0 3px;
|
|
19
26
|
}
|
|
20
|
-
|
|
27
|
+
.tab {
|
|
28
|
+
appearance: none;
|
|
29
|
+
display: block;
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
width: 100%;
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
border-radius: 0;
|
|
35
|
+
font: inherit;
|
|
36
|
+
letter-spacing: inherit;
|
|
37
|
+
word-spacing: inherit;
|
|
38
|
+
text-align: inherit;
|
|
39
|
+
text-transform: inherit;
|
|
40
|
+
text-decoration: none;
|
|
21
41
|
border-top-left-radius: var(--dile-tab-border-radius, 4px);
|
|
22
42
|
border-top-right-radius: var(--dile-tab-border-radius, 4px);
|
|
23
43
|
transition: all 0.3s ease;
|
|
@@ -28,7 +48,13 @@ export class DileTab extends DileSelectableItem(LitElement) {
|
|
|
28
48
|
font-weight: var(--dile-tab-font-weight, normal);
|
|
29
49
|
font-size: var(--dile-tab-font-size, 1rem);
|
|
30
50
|
}
|
|
31
|
-
|
|
51
|
+
.tab:focus-visible {
|
|
52
|
+
outline: var(--dile-tab-focus-ring-width, 2px) solid var(--dile-tab-focus-ring-color, currentColor);
|
|
53
|
+
outline-offset: var(--dile-tab-focus-ring-offset, -2px);
|
|
54
|
+
transition-property: background-color, color;
|
|
55
|
+
}
|
|
56
|
+
.label {
|
|
57
|
+
display: block;
|
|
32
58
|
padding: var(--dile-tab-padding, 8px 12px 6px 12px);
|
|
33
59
|
text-transform: var(--dile-tab-text-transform, uppercase);
|
|
34
60
|
white-space: nowrap;
|
|
@@ -38,7 +64,7 @@ export class DileTab extends DileSelectableItem(LitElement) {
|
|
|
38
64
|
color: var(--dile-tab-selected-text-color, var(--dile-on-primary-dark-color, #000));
|
|
39
65
|
border: var(--dile-tab-selected-border, none);
|
|
40
66
|
}
|
|
41
|
-
|
|
67
|
+
.mark {
|
|
42
68
|
display: block;
|
|
43
69
|
height: var(--dile-tab-selected-line-height, 5px);
|
|
44
70
|
width: 0;
|
|
@@ -48,11 +74,6 @@ export class DileTab extends DileSelectableItem(LitElement) {
|
|
|
48
74
|
.markselected {
|
|
49
75
|
width: 100%;
|
|
50
76
|
}
|
|
51
|
-
a {
|
|
52
|
-
display: block;
|
|
53
|
-
color: inherit;
|
|
54
|
-
text-decoration: none;
|
|
55
|
-
}
|
|
56
77
|
.line {
|
|
57
78
|
display: flex;
|
|
58
79
|
justify-content: center;
|
|
@@ -61,22 +82,55 @@ export class DileTab extends DileSelectableItem(LitElement) {
|
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
render() {
|
|
85
|
+
const tabClass = `tab ${this.selected ? 'selected' : ''}`;
|
|
86
|
+
const ariaSelected = this.selected ? 'true' : 'false';
|
|
87
|
+
const tabindex = this.tabbable ? '0' : '-1';
|
|
64
88
|
if (this.href) {
|
|
65
89
|
return html`
|
|
66
|
-
<a
|
|
90
|
+
<a
|
|
91
|
+
href="${this.href}"
|
|
92
|
+
target="${ifDefined(this.target)}"
|
|
93
|
+
class="${tabClass}"
|
|
94
|
+
role="tab"
|
|
95
|
+
aria-selected="${ariaSelected}"
|
|
96
|
+
tabindex="${tabindex}"
|
|
97
|
+
@click="${this.select}"
|
|
98
|
+
@keydown="${this._onLinkKeydown}"
|
|
99
|
+
>${this.tabTemplate}</a>
|
|
67
100
|
`;
|
|
68
101
|
}
|
|
69
|
-
return
|
|
102
|
+
return html`
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
class="${tabClass}"
|
|
106
|
+
role="tab"
|
|
107
|
+
aria-selected="${ariaSelected}"
|
|
108
|
+
tabindex="${tabindex}"
|
|
109
|
+
@click="${this.select}"
|
|
110
|
+
>${this.tabTemplate}</button>
|
|
111
|
+
`;
|
|
70
112
|
}
|
|
71
113
|
|
|
72
114
|
get tabTemplate() {
|
|
73
115
|
return html`
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
</div>
|
|
79
|
-
</article>
|
|
116
|
+
<span class="label"><slot></slot></span>
|
|
117
|
+
<span class="line">
|
|
118
|
+
<span class="mark ${this.selected ? 'markselected' : ''}"></span>
|
|
119
|
+
</span>
|
|
80
120
|
`;
|
|
81
121
|
}
|
|
122
|
+
|
|
123
|
+
// A native link is only activated with Enter
|
|
124
|
+
_onLinkKeydown(e) {
|
|
125
|
+
if (e.key === ' ') {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
if (!e.repeat) {
|
|
128
|
+
e.currentTarget.click();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
focus(options) {
|
|
134
|
+
this.shadowRoot?.querySelector('.tab')?.focus(options);
|
|
135
|
+
}
|
|
82
136
|
}
|
|
@@ -9,4 +9,67 @@ export class DileTabs extends DileSelectable(LitElement) {
|
|
|
9
9
|
}
|
|
10
10
|
`;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.addEventListener('keydown', this._onKeydown);
|
|
16
|
+
this.addEventListener('focusin', () => this._updateTabbable());
|
|
17
|
+
this.addEventListener('focusout', this._onFocusOut);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connectedCallback() {
|
|
21
|
+
super.connectedCallback();
|
|
22
|
+
if (!this.hasAttribute('role')) {
|
|
23
|
+
this.setAttribute('role', 'tablist');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render() {
|
|
28
|
+
return html`<slot @slotchange="${() => this._updateTabbable()}"></slot>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setSelectedItem() {
|
|
32
|
+
super.setSelectedItem();
|
|
33
|
+
this._updateTabbable();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Roving tabindex: the tablist is only one stop in the page tab sequence.
|
|
37
|
+
_updateTabbable(ignoreFocus = false) {
|
|
38
|
+
const tabs = this.getItems();
|
|
39
|
+
const focused = ignoreFocus ? undefined : tabs.find((tab) => tab.matches(':focus-within'));
|
|
40
|
+
const tabbable = focused ?? tabs.find((tab) => tab.selected) ?? tabs[0];
|
|
41
|
+
tabs.forEach((tab) => {
|
|
42
|
+
tab.tabbable = tab === tabbable;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_onFocusOut(e) {
|
|
47
|
+
if (!e.relatedTarget || !this.contains(e.relatedTarget)) {
|
|
48
|
+
this._updateTabbable(true);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The arrows only move the focus. Enter or Space select the focused tab, so moving
|
|
53
|
+
// through the tabs doesn't dispatch changes or follow the links of the tabs with href.
|
|
54
|
+
_onKeydown(e) {
|
|
55
|
+
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (e.composedPath()[0].getAttribute?.('role') !== 'tab') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const tabs = this.getItems();
|
|
62
|
+
const current = tabs.findIndex((tab) => tab.matches(':focus-within'));
|
|
63
|
+
const step = getComputedStyle(this).direction === 'rtl' ? -1 : 1;
|
|
64
|
+
const nextIndex = {
|
|
65
|
+
ArrowRight: (current + step + tabs.length) % tabs.length,
|
|
66
|
+
ArrowLeft: (current - step + tabs.length) % tabs.length,
|
|
67
|
+
Home: 0,
|
|
68
|
+
End: tabs.length - 1,
|
|
69
|
+
}[e.key];
|
|
70
|
+
if (tabs[nextIndex]) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
tabs[nextIndex].focus();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
12
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.18.
|
|
3
|
+
"version": "3.18.4",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "ceb56100a952af7551b43f709418bf0848962ef3"
|
|
32
32
|
}
|