@api-client/ui 0.3.1 → 0.3.3
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/build/src/elements/environment/EnvironmentEditor.d.ts.map +1 -1
- package/build/src/elements/environment/EnvironmentEditor.js +8 -6
- package/build/src/elements/environment/EnvironmentEditor.js.map +1 -1
- package/build/src/elements/har/HarViewer.d.ts.map +1 -1
- package/build/src/elements/har/HarViewer.js +13 -15
- package/build/src/elements/har/HarViewer.js.map +1 -1
- package/build/src/elements/http/RequestEditor.d.ts +2 -1
- package/build/src/elements/http/RequestEditor.d.ts.map +1 -1
- package/build/src/elements/http/RequestEditor.js +17 -12
- package/build/src/elements/http/RequestEditor.js.map +1 -1
- package/build/src/elements/http/RequestLog.d.ts.map +1 -1
- package/build/src/elements/http/RequestLog.js +34 -8
- package/build/src/elements/http/RequestLog.js.map +1 -1
- package/build/src/md/button/internals/button.styles.js +4 -4
- package/build/src/md/button/internals/button.styles.js.map +1 -1
- package/build/src/md/motion/animation.d.ts +5 -3
- package/build/src/md/motion/animation.d.ts.map +1 -1
- package/build/src/md/motion/animation.js +4 -2
- package/build/src/md/motion/animation.js.map +1 -1
- package/build/src/md/ripple/internals/ripple.styles.d.ts.map +1 -1
- package/build/src/md/ripple/internals/ripple.styles.js +20 -8
- package/build/src/md/ripple/internals/ripple.styles.js.map +1 -1
- package/build/src/md/tabs/internals/Tab.d.ts +25 -9
- package/build/src/md/tabs/internals/Tab.d.ts.map +1 -1
- package/build/src/md/tabs/internals/Tab.js +122 -53
- package/build/src/md/tabs/internals/Tab.js.map +1 -1
- package/build/src/md/tabs/internals/Tab.styles.d.ts.map +1 -1
- package/build/src/md/tabs/internals/Tab.styles.js +69 -64
- package/build/src/md/tabs/internals/Tab.styles.js.map +1 -1
- package/build/src/md/tabs/internals/Tabs.d.ts +52 -54
- package/build/src/md/tabs/internals/Tabs.d.ts.map +1 -1
- package/build/src/md/tabs/internals/Tabs.js +270 -330
- package/build/src/md/tabs/internals/Tabs.js.map +1 -1
- package/build/src/md/tabs/internals/Tabs.styles.d.ts.map +1 -1
- package/build/src/md/tabs/internals/Tabs.styles.js +13 -17
- package/build/src/md/tabs/internals/Tabs.styles.js.map +1 -1
- package/build/src/md/text-field/internals/common.styles.d.ts.map +1 -1
- package/build/src/md/text-field/internals/common.styles.js +0 -3
- package/build/src/md/text-field/internals/common.styles.js.map +1 -1
- package/build/src/styles/m3/native.css +270 -0
- package/build/src/styles/m3/theme.css +155 -0
- package/build/src/styles/m3/tokens.css +512 -0
- package/demo/md/tabs/tabs.html +19 -0
- package/demo/md/tabs/tabs.ts +133 -83
- package/package.json +20 -4
- package/scripts/copy-assets.js +21 -0
- package/src/elements/environment/EnvironmentEditor.ts +8 -6
- package/src/elements/har/HarViewer.ts +13 -15
- package/src/elements/http/RequestEditor.ts +18 -13
- package/src/elements/http/RequestLog.ts +34 -8
- package/src/md/button/internals/button.styles.ts +4 -4
- package/src/md/motion/animation.ts +4 -2
- package/src/md/ripple/internals/ripple.styles.ts +20 -8
- package/src/md/tabs/internals/Tab.styles.ts +69 -64
- package/src/md/tabs/internals/Tab.ts +126 -43
- package/src/md/tabs/internals/Tabs.styles.ts +13 -17
- package/src/md/tabs/internals/Tabs.ts +259 -305
- package/src/md/text-field/internals/common.styles.ts +0 -3
- package/test/elements/har/HarViewerElement.test.ts +1 -55
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { html, LitElement,
|
|
1
|
+
import { html, LitElement, PropertyValues, TemplateResult } from 'lit'
|
|
2
2
|
import { property, query, queryAssignedElements, state } from 'lit/decorators.js'
|
|
3
3
|
import { ClassInfo, classMap } from 'lit/directives/class-map.js'
|
|
4
4
|
import { StyleInfo, styleMap } from 'lit/directives/style-map.js'
|
|
@@ -11,9 +11,19 @@ import '../../icons/ui-icon.js'
|
|
|
11
11
|
export type TabsPriority = 'primary' | 'secondary'
|
|
12
12
|
|
|
13
13
|
export interface TabSelectionDetail {
|
|
14
|
+
/**
|
|
15
|
+
* The selected tab.
|
|
16
|
+
*/
|
|
14
17
|
item: UiTab
|
|
18
|
+
/**
|
|
19
|
+
* The index of the selected tab.
|
|
20
|
+
*/
|
|
15
21
|
index: number
|
|
16
|
-
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SizingInfo {
|
|
25
|
+
left: number
|
|
26
|
+
width: number
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
/**
|
|
@@ -29,61 +39,91 @@ export function calcPercent(w: number, w0: number): number {
|
|
|
29
39
|
/**
|
|
30
40
|
* A container for tabs.
|
|
31
41
|
*
|
|
32
|
-
* @fires
|
|
33
|
-
* The `event.detail` object contains the `item` and `index` properties.
|
|
34
|
-
* It also has the `select` property with the value of the `selectedAttribute` on the tab.
|
|
42
|
+
* @fires change - A non bubbling event when selection change through user interaction.
|
|
35
43
|
*/
|
|
36
44
|
export default class UiTabs extends LitElement {
|
|
37
|
-
items: UiTab[] = []
|
|
38
|
-
|
|
39
45
|
activeItem: UiTab | null = null
|
|
40
46
|
|
|
41
47
|
previousItem: UiTab | null = null
|
|
42
48
|
|
|
43
|
-
@queryAssignedElements({ flatten: true })
|
|
44
|
-
protected accessor assignedElements!: HTMLElement[]
|
|
45
|
-
|
|
46
|
-
@query('.pointer') protected accessor pointer!: HTMLElement
|
|
47
|
-
|
|
48
|
-
@query('.content') protected accessor content!: HTMLElement
|
|
49
|
-
|
|
50
49
|
/**
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
53
|
-
* @default primary
|
|
54
|
-
* @attribute
|
|
50
|
+
* The currently selected tab, `null` only when there are no tab children.
|
|
55
51
|
*/
|
|
56
|
-
|
|
52
|
+
get activeTab(): UiTab | null {
|
|
53
|
+
return this.tabs.find((tab) => tab.selected) ?? null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set activeTab(tab: UiTab | null) {
|
|
57
|
+
// Ignore setting activeTab to null. As long as there are children, one tab
|
|
58
|
+
// must be selected.
|
|
59
|
+
if (tab) {
|
|
60
|
+
this.activateTab(tab)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
/**
|
|
59
|
-
*
|
|
60
|
-
* @attribute
|
|
65
|
+
* The index of the currently selected tab.
|
|
61
66
|
*/
|
|
62
|
-
|
|
67
|
+
get activeTabIndex() {
|
|
68
|
+
return this.tabs.findIndex((tab) => tab.selected)
|
|
69
|
+
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
|
-
*
|
|
66
|
-
* This is matched with the `aria-controls` of the tab.
|
|
67
|
-
* @attribute
|
|
72
|
+
* Sets the active tab by index.
|
|
68
73
|
*/
|
|
69
|
-
|
|
74
|
+
set activeTabIndex(index) {
|
|
75
|
+
const activateTabAtIndex = () => {
|
|
76
|
+
const tab = this.tabs[index]
|
|
77
|
+
// Ignore out-of-bound indices.
|
|
78
|
+
if (tab) {
|
|
79
|
+
this.activateTab(tab)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!this.slotElement) {
|
|
83
|
+
// This is needed to support setting the activeTabIndex via a lit property
|
|
84
|
+
// binding.
|
|
85
|
+
//
|
|
86
|
+
// ```ts
|
|
87
|
+
// html`
|
|
88
|
+
// <md-tabs .activeTabIndex=${1}>
|
|
89
|
+
// <md-tab>First</md-tab>
|
|
90
|
+
// <md-tab>Second</md-tab>
|
|
91
|
+
// </md-tabs>
|
|
92
|
+
// `;
|
|
93
|
+
// ```
|
|
94
|
+
//
|
|
95
|
+
// It's needed since lit's rendering lifecycle is asynchronous, and the
|
|
96
|
+
// `<slot>` element hasn't rendered, so `tabs` is empty.
|
|
97
|
+
this.updateComplete.then(activateTabAtIndex)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
activateTabAtIndex()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get focusedTab() {
|
|
104
|
+
return this.tabs.find((tab) => tab.matches(':focus-within'))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@queryAssignedElements({ flatten: true, selector: 'ui-tab' }) private accessor tabs!: UiTab[]
|
|
108
|
+
|
|
109
|
+
@query('.tabs') private accessor tabsScrollerElement!: HTMLElement
|
|
110
|
+
@query('slot') private accessor slotElement!: HTMLSlotElement
|
|
111
|
+
|
|
112
|
+
@query('.pointer') private accessor pointer!: HTMLElement
|
|
70
113
|
|
|
71
114
|
/**
|
|
72
|
-
* The
|
|
73
|
-
* corresponds to which tab.
|
|
115
|
+
* The priority of the tabs.
|
|
74
116
|
*
|
|
75
|
-
* @default
|
|
117
|
+
* @default primary
|
|
76
118
|
* @attribute
|
|
77
119
|
*/
|
|
78
|
-
@property({ type: String }) accessor
|
|
79
|
-
|
|
80
|
-
@state() protected accessor pointerStyles: StyleInfo | undefined
|
|
120
|
+
@property({ type: String, reflect: true }) accessor priority: TabsPriority = 'primary'
|
|
81
121
|
|
|
82
|
-
|
|
122
|
+
@state() private accessor pointerStyles: StyleInfo | undefined
|
|
83
123
|
|
|
84
|
-
@state()
|
|
124
|
+
@state() private accessor indicated = false
|
|
85
125
|
|
|
86
|
-
|
|
126
|
+
private observer: IntersectionObserver
|
|
87
127
|
|
|
88
128
|
/**
|
|
89
129
|
* This is set by the intersection observer. Once the tabs are in the view it turns to `true`.
|
|
@@ -91,13 +131,23 @@ export default class UiTabs extends LitElement {
|
|
|
91
131
|
*/
|
|
92
132
|
@state() accessor isVisible: boolean | undefined
|
|
93
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether or not to automatically select a tab when it is focused.
|
|
136
|
+
*/
|
|
137
|
+
@property({ type: Boolean }) accessor autoActivate = false
|
|
138
|
+
|
|
139
|
+
private readonly internals = (this as HTMLElement).attachInternals()
|
|
140
|
+
|
|
94
141
|
constructor() {
|
|
95
142
|
super()
|
|
96
|
-
|
|
143
|
+
this.internals.role = 'tablist'
|
|
97
144
|
this.observer = new IntersectionObserver(this.intersectionCallback.bind(this), {
|
|
98
145
|
threshold: 1.0,
|
|
99
146
|
rootMargin: '0px',
|
|
100
147
|
})
|
|
148
|
+
this.addEventListener('keydown', this.handleKeydown.bind(this))
|
|
149
|
+
this.addEventListener('keyup', this.handleKeyup.bind(this))
|
|
150
|
+
this.addEventListener('focusout', this.handleFocusout.bind(this))
|
|
101
151
|
}
|
|
102
152
|
|
|
103
153
|
protected override willUpdate(cp: PropertyValues<this>): void {
|
|
@@ -109,10 +159,10 @@ export default class UiTabs extends LitElement {
|
|
|
109
159
|
|
|
110
160
|
override connectedCallback(): void {
|
|
111
161
|
super.connectedCallback()
|
|
162
|
+
this.observer.observe(this)
|
|
112
163
|
if (!this.hasAttribute('role')) {
|
|
113
164
|
this.setAttribute('role', 'tablist')
|
|
114
165
|
}
|
|
115
|
-
this.observer.observe(this)
|
|
116
166
|
}
|
|
117
167
|
|
|
118
168
|
override disconnectedCallback(): void {
|
|
@@ -125,106 +175,29 @@ export default class UiTabs extends LitElement {
|
|
|
125
175
|
this.isVisible = entry.isIntersecting
|
|
126
176
|
}
|
|
127
177
|
|
|
128
|
-
protected async updateItems(): Promise<void> {
|
|
129
|
-
const elements = this.assignedElements || []
|
|
130
|
-
const items = elements.filter(this.isTabItem, this)
|
|
131
|
-
this.items = items
|
|
132
|
-
if (this.activeItem && !items.includes(this.activeItem)) {
|
|
133
|
-
this.activeItem = null
|
|
134
|
-
}
|
|
135
|
-
await this.updateComplete
|
|
136
|
-
this.ensureSelection()
|
|
137
|
-
}
|
|
138
|
-
|
|
139
178
|
/**
|
|
140
|
-
*
|
|
179
|
+
* Scrolls the toolbar, if overflowing, to the active tab, or the provided
|
|
180
|
+
* tab.
|
|
181
|
+
*
|
|
182
|
+
* @param tabToScrollTo The tab that should be scrolled to. Defaults to the
|
|
183
|
+
* active tab.
|
|
184
|
+
* @return A Promise that resolves after the tab has been scrolled to.
|
|
141
185
|
*/
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
protected isSelectable(element: UiTab): boolean {
|
|
150
|
-
if (element.disabled) {
|
|
151
|
-
return false
|
|
152
|
-
}
|
|
153
|
-
if (element.hidden && element.hasAttribute('hidden')) {
|
|
154
|
-
return false
|
|
155
|
-
}
|
|
156
|
-
return true
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
protected ensureSelection(): void {
|
|
160
|
-
const { selected, selectedAttribute, activeItem, items } = this
|
|
161
|
-
if (!selectedAttribute) {
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
const item = items.find((i) => i.getAttribute(selectedAttribute) === selected)
|
|
165
|
-
if (item && item === activeItem) {
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
this.makeSelection(item)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
protected makeSelection(tab?: UiTab, focus = false): void {
|
|
172
|
-
const { activeItem } = this
|
|
173
|
-
if (activeItem === tab) {
|
|
174
|
-
tab.highlight()
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
this.previousItem = activeItem
|
|
178
|
-
if (activeItem) {
|
|
179
|
-
this.deselectItem(activeItem)
|
|
180
|
-
}
|
|
181
|
-
if (tab) {
|
|
182
|
-
this.selectItem(tab, focus)
|
|
183
|
-
this.positionPointer(tab, activeItem)
|
|
184
|
-
if (this.activeItem) {
|
|
185
|
-
// we set this here so we won't notify selection when initializing.
|
|
186
|
-
this.notifySelect(tab)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
notifySelect(item: UiTab): void {
|
|
192
|
-
const index = this.items.indexOf(item)
|
|
193
|
-
if (index === -1) {
|
|
186
|
+
async scrollToTab(tabToScrollTo?: UiTab | null) {
|
|
187
|
+
await this.updateComplete
|
|
188
|
+
const { tabs } = this
|
|
189
|
+
tabToScrollTo ??= this.activeTab
|
|
190
|
+
if (!tabToScrollTo || !tabs.includes(tabToScrollTo) || !this.tabsScrollerElement) {
|
|
194
191
|
return
|
|
195
192
|
}
|
|
196
|
-
const attrValue = item.getAttribute(this.selectedAttribute)
|
|
197
|
-
this.dispatchEvent(
|
|
198
|
-
new CustomEvent<TabSelectionDetail>('select', {
|
|
199
|
-
detail: {
|
|
200
|
-
item,
|
|
201
|
-
index,
|
|
202
|
-
select: attrValue,
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
protected selectItem(tab: UiTab, focus = false): void {
|
|
209
|
-
this.activeItem = tab
|
|
210
|
-
if (this.hasAttribute('tabindex')) {
|
|
211
|
-
this.removeAttribute('tabindex')
|
|
212
|
-
}
|
|
213
|
-
tab.setAttribute('aria-selected', 'true')
|
|
214
|
-
tab.setAttribute('tabindex', '0')
|
|
215
|
-
tab.selected = true
|
|
216
|
-
tab.priority = this.priority
|
|
217
|
-
tab.scrollIntoView({ inline: 'nearest', behavior: 'auto', block: 'nearest' })
|
|
218
|
-
if (focus) {
|
|
219
|
-
tab.focus()
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
193
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
194
|
+
// wait for tabs to render.
|
|
195
|
+
await Promise.all(tabs.map((tab) => tab.updateComplete))
|
|
196
|
+
tabToScrollTo.scrollIntoView({
|
|
197
|
+
block: 'nearest',
|
|
198
|
+
inline: 'nearest',
|
|
199
|
+
behavior: !this.focusedTab ? 'instant' : 'auto',
|
|
200
|
+
})
|
|
228
201
|
}
|
|
229
202
|
|
|
230
203
|
protected handleVisibility(): void {
|
|
@@ -236,11 +209,12 @@ export default class UiTabs extends LitElement {
|
|
|
236
209
|
}
|
|
237
210
|
|
|
238
211
|
protected async positionPointer(tab: UiTab, old?: UiTab | null): Promise<void> {
|
|
239
|
-
const {
|
|
212
|
+
const { pointer, isVisible } = this
|
|
240
213
|
if (!isVisible) {
|
|
214
|
+
tab.indicated = true
|
|
241
215
|
return
|
|
242
216
|
}
|
|
243
|
-
const index =
|
|
217
|
+
const index = this.tabs.indexOf(tab)
|
|
244
218
|
if (index < 0 || !pointer) {
|
|
245
219
|
this.pointerStyles = undefined
|
|
246
220
|
return
|
|
@@ -252,29 +226,31 @@ export default class UiTabs extends LitElement {
|
|
|
252
226
|
return
|
|
253
227
|
}
|
|
254
228
|
const isPrimary = this.priority === 'primary'
|
|
255
|
-
const final =
|
|
256
|
-
if (this.pointerStyles && this.pointerStyles.left === `${final}px`) {
|
|
229
|
+
const final = this.getTabSizing(tab)
|
|
230
|
+
if (this.pointerStyles && this.pointerStyles.left === `${final.left}px`) {
|
|
231
|
+
tab.indicated = true
|
|
257
232
|
return
|
|
258
233
|
}
|
|
259
234
|
|
|
260
235
|
// first position this indicator in the place of the old one.
|
|
261
236
|
// update the view and then run the animation.
|
|
262
237
|
this.indicated = true
|
|
263
|
-
const
|
|
264
|
-
this.pointerStyles = { left }
|
|
238
|
+
const starting = this.getTabSizing(old)
|
|
239
|
+
this.pointerStyles = { left: `${starting.left}px`, width: `${starting.width}px` }
|
|
265
240
|
await this.updateComplete
|
|
266
|
-
const frames = isPrimary ? this.getPrimaryKeyframes(
|
|
241
|
+
const frames = isPrimary ? this.getPrimaryKeyframes(starting, final) : this.getSecondaryKeyframes(starting, final)
|
|
267
242
|
|
|
268
243
|
if (this.moveAnimation) {
|
|
269
244
|
this.moveAnimation.cancel()
|
|
270
245
|
}
|
|
271
246
|
const moveAnimation = pointer.animate(frames, {
|
|
272
|
-
duration:
|
|
247
|
+
duration: 200,
|
|
273
248
|
iterations: 1,
|
|
274
|
-
easing: Easing.
|
|
249
|
+
easing: Easing.EMPHASIZED_DECELERATE,
|
|
275
250
|
})
|
|
276
251
|
const finalStyles: StyleInfo = {
|
|
277
|
-
left: `${final}px`,
|
|
252
|
+
left: `${final.left}px`,
|
|
253
|
+
width: `${final.width}px`,
|
|
278
254
|
}
|
|
279
255
|
moveAnimation.addEventListener('finish', () => {
|
|
280
256
|
this.pointerStyles = finalStyles
|
|
@@ -290,198 +266,194 @@ export default class UiTabs extends LitElement {
|
|
|
290
266
|
|
|
291
267
|
protected moveAnimation?: Animation
|
|
292
268
|
|
|
293
|
-
protected
|
|
294
|
-
const contentBox = this.
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const left = leftFromParent + tabRect.width / 2 - 20 + offset
|
|
299
|
-
return `${left}px`
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
protected getSecondaryLeft(tab: UiTab): string {
|
|
303
|
-
const contentBox = this.content.getBoundingClientRect()
|
|
304
|
-
const tabRect = tab.getBoundingClientRect()
|
|
305
|
-
const offset = this.scrollable ? 48 : 0 // left button
|
|
306
|
-
const leftFromParent = tabRect.x - contentBox.x + offset
|
|
307
|
-
return `${leftFromParent}px`
|
|
269
|
+
protected getTabSizing(tab: UiTab): SizingInfo {
|
|
270
|
+
const contentBox = this.tabsScrollerElement.getBoundingClientRect()
|
|
271
|
+
const sizing = tab.getIndicatorSizing()
|
|
272
|
+
sizing.left = sizing.left - contentBox.x
|
|
273
|
+
return sizing
|
|
308
274
|
}
|
|
309
275
|
|
|
310
|
-
protected getPrimaryKeyframes(start:
|
|
276
|
+
protected getPrimaryKeyframes(start: SizingInfo, final: SizingInfo): Keyframe[] {
|
|
311
277
|
return [
|
|
312
278
|
{
|
|
313
|
-
left: start
|
|
314
|
-
width:
|
|
279
|
+
left: `${start.left}px`,
|
|
280
|
+
width: `${start.width}px`,
|
|
315
281
|
},
|
|
316
282
|
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
{
|
|
320
|
-
left: final,
|
|
321
|
-
width: `40px`,
|
|
283
|
+
left: `${final.left}px`,
|
|
284
|
+
width: `${final.width}px`,
|
|
322
285
|
},
|
|
323
286
|
]
|
|
324
287
|
}
|
|
325
288
|
|
|
326
|
-
protected getSecondaryKeyframes(start:
|
|
289
|
+
protected getSecondaryKeyframes(start: SizingInfo, final: SizingInfo): Keyframe[] {
|
|
327
290
|
return [
|
|
328
291
|
{
|
|
329
|
-
left: start
|
|
292
|
+
left: `${start.left}px`,
|
|
293
|
+
width: `${start.width}px`,
|
|
330
294
|
},
|
|
331
295
|
{
|
|
332
|
-
left: final
|
|
296
|
+
left: `${final.left}px`,
|
|
297
|
+
width: `${final.width}px`,
|
|
333
298
|
},
|
|
334
299
|
]
|
|
335
300
|
}
|
|
336
301
|
|
|
337
|
-
protected
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.activateFromEvent(e)
|
|
302
|
+
protected async handleTabClick(e: PointerEvent): Promise<void> {
|
|
303
|
+
const tab = e.composedPath().find((el) => isTab(el)) as UiTab | undefined
|
|
304
|
+
// Allow event to bubble
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
306
|
+
if (e.defaultPrevented || !tab || tab.selected) {
|
|
307
|
+
return
|
|
344
308
|
}
|
|
309
|
+
this.activateTab(tab)
|
|
345
310
|
}
|
|
346
311
|
|
|
347
|
-
|
|
348
|
-
const {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
312
|
+
private activateTab(activeTab: UiTab): void {
|
|
313
|
+
const { tabs } = this
|
|
314
|
+
const previousTab = this.activeTab
|
|
315
|
+
if (!tabs.includes(activeTab) || previousTab === activeTab) {
|
|
316
|
+
// Ignore setting activeTab to a tab element that is not a child.
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
for (const tab of tabs) {
|
|
320
|
+
tab.selected = tab === activeTab
|
|
321
|
+
}
|
|
322
|
+
if (previousTab) {
|
|
323
|
+
// Don't dispatch a change event if activating a tab when no previous tabs
|
|
324
|
+
// were selected, such as when md-tabs auto-selects the first tab.
|
|
325
|
+
const detail: TabSelectionDetail = {
|
|
326
|
+
item: activeTab,
|
|
327
|
+
index: this.tabs.indexOf(activeTab),
|
|
355
328
|
}
|
|
356
|
-
|
|
357
|
-
|
|
329
|
+
const defaultPrevented = !this.dispatchEvent(
|
|
330
|
+
new CustomEvent('change', { detail, bubbles: false, cancelable: true })
|
|
331
|
+
)
|
|
332
|
+
if (defaultPrevented) {
|
|
333
|
+
for (const tab of tabs) {
|
|
334
|
+
tab.selected = tab === previousTab
|
|
335
|
+
}
|
|
336
|
+
return
|
|
358
337
|
}
|
|
338
|
+
previousTab.indicated = false
|
|
359
339
|
}
|
|
360
|
-
|
|
361
|
-
|
|
340
|
+
activeTab.indicated = false
|
|
341
|
+
this.positionPointer(activeTab, previousTab)
|
|
342
|
+
this.updateFocusableTab(activeTab)
|
|
343
|
+
this.scrollToTab(activeTab)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private updateFocusableTab(focusableTab: UiTab) {
|
|
347
|
+
for (const tab of this.tabs) {
|
|
348
|
+
tab.tabIndex = tab === focusableTab ? 0 : -1
|
|
362
349
|
}
|
|
363
|
-
this.makeSelection(item, true)
|
|
364
350
|
}
|
|
365
351
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
352
|
+
private async handleKeydown(event: KeyboardEvent) {
|
|
353
|
+
// Allow event to bubble.
|
|
354
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
355
|
+
const isLeft = event.key === 'ArrowLeft'
|
|
356
|
+
const isRight = event.key === 'ArrowRight'
|
|
357
|
+
const isHome = event.key === 'Home'
|
|
358
|
+
const isEnd = event.key === 'End'
|
|
359
|
+
// Ignore non-navigation keys
|
|
360
|
+
if (event.defaultPrevented || (!isLeft && !isRight && !isHome && !isEnd)) {
|
|
369
361
|
return
|
|
370
362
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
363
|
+
|
|
364
|
+
const { tabs } = this
|
|
365
|
+
// Don't try to select another tab if there aren't any.
|
|
366
|
+
if (tabs.length < 2) {
|
|
367
|
+
return
|
|
374
368
|
}
|
|
375
|
-
this.contentScroll = left
|
|
376
|
-
content.scrollTo({
|
|
377
|
-
behavior: 'smooth',
|
|
378
|
-
left,
|
|
379
|
-
})
|
|
380
|
-
}
|
|
381
369
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
370
|
+
// Prevent default interactions, such as scrolling.
|
|
371
|
+
event.preventDefault()
|
|
372
|
+
|
|
373
|
+
let indexToFocus: number
|
|
374
|
+
if (isHome || isEnd) {
|
|
375
|
+
indexToFocus = isHome ? 0 : tabs.length - 1
|
|
376
|
+
} else {
|
|
377
|
+
// Check if moving forwards or backwards
|
|
378
|
+
const isRtl = getComputedStyle(this).direction === 'rtl'
|
|
379
|
+
const forwards = isRtl ? isLeft : isRight
|
|
380
|
+
const { focusedTab } = this
|
|
381
|
+
if (!focusedTab) {
|
|
382
|
+
// If there is not already a tab focused, select the first or last tab
|
|
383
|
+
// based on the direction we're traveling.
|
|
384
|
+
indexToFocus = forwards ? 0 : tabs.length - 1
|
|
385
|
+
} else {
|
|
386
|
+
const focusedIndex = this.tabs.indexOf(focusedTab)
|
|
387
|
+
indexToFocus = forwards ? focusedIndex + 1 : focusedIndex - 1
|
|
388
|
+
if (indexToFocus >= tabs.length) {
|
|
389
|
+
// Return to start if moving past the last item.
|
|
390
|
+
indexToFocus = 0
|
|
391
|
+
} else if (indexToFocus < 0) {
|
|
392
|
+
// Go to end if moving before the first item.
|
|
393
|
+
indexToFocus = tabs.length - 1
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const tabToFocus = tabs[indexToFocus]
|
|
399
|
+
tabToFocus.focus()
|
|
400
|
+
if (this.autoActivate) {
|
|
401
|
+
this.activateTab(tabToFocus)
|
|
402
|
+
} else {
|
|
403
|
+
this.updateFocusableTab(tabToFocus)
|
|
387
404
|
}
|
|
388
|
-
this.contentScroll = left
|
|
389
|
-
content.scrollTo({
|
|
390
|
-
behavior: 'smooth',
|
|
391
|
-
left,
|
|
392
|
-
})
|
|
393
405
|
}
|
|
394
406
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.
|
|
407
|
+
// scroll to item on keyup.
|
|
408
|
+
private handleKeyup() {
|
|
409
|
+
this.scrollToTab(this.focusedTab ?? this.activeTab)
|
|
398
410
|
}
|
|
399
411
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
this.makeSelection(tab, true)
|
|
405
|
-
} else if (e.code === 'ArrowLeft') {
|
|
406
|
-
e.preventDefault()
|
|
407
|
-
const tab = this.activeItem ? this.getPreviousItem(this.activeItem) : this.items[0]
|
|
408
|
-
this.makeSelection(tab, true)
|
|
412
|
+
private handleFocusout() {
|
|
413
|
+
// restore focus to selected item when blurring the tab bar.
|
|
414
|
+
if (this.matches(':focus-within')) {
|
|
415
|
+
return
|
|
409
416
|
}
|
|
410
|
-
}
|
|
411
417
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (curIndex < 0) {
|
|
416
|
-
return item
|
|
418
|
+
const { activeTab } = this
|
|
419
|
+
if (activeTab) {
|
|
420
|
+
this.updateFocusableTab(activeTab)
|
|
417
421
|
}
|
|
418
|
-
let i = curIndex
|
|
419
|
-
let result: HTMLElement | undefined
|
|
420
|
-
do {
|
|
421
|
-
i--
|
|
422
|
-
if (i === curIndex) {
|
|
423
|
-
// looped back from the end, no active element to find.
|
|
424
|
-
return item
|
|
425
|
-
}
|
|
426
|
-
const tmp = items[i]
|
|
427
|
-
if (!tmp) {
|
|
428
|
-
i = items.length
|
|
429
|
-
continue
|
|
430
|
-
}
|
|
431
|
-
if (this.isSelectable(tmp)) {
|
|
432
|
-
result = tmp
|
|
433
|
-
}
|
|
434
|
-
} while (!result)
|
|
435
|
-
return (result as UiTab) || item
|
|
436
422
|
}
|
|
437
423
|
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
let i = curIndex
|
|
445
|
-
let next: HTMLElement | undefined
|
|
446
|
-
do {
|
|
447
|
-
i++
|
|
448
|
-
if (i === curIndex) {
|
|
449
|
-
// looped back from the start, no active element to find.
|
|
450
|
-
return item
|
|
451
|
-
}
|
|
452
|
-
const tmp = items[i]
|
|
453
|
-
if (!tmp) {
|
|
454
|
-
i = -1
|
|
455
|
-
continue
|
|
424
|
+
private handleSlotChange() {
|
|
425
|
+
for (const tab of this.tabs) {
|
|
426
|
+
tab.priority = this.priority
|
|
427
|
+
if (tab.selected) {
|
|
428
|
+
tab.indicated = true
|
|
456
429
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
430
|
+
}
|
|
431
|
+
const firstTab = this.tabs[0]
|
|
432
|
+
if (!this.activeTab && firstTab) {
|
|
433
|
+
// If the active tab was removed, auto-select the first one. There should
|
|
434
|
+
// always be a selected tab while the bar has children.
|
|
435
|
+
this.activateTab(firstTab)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// When children shift, ensure the active tab is visible. For example, if
|
|
439
|
+
// many children are added before the active tab, it'd be pushed off screen.
|
|
440
|
+
// This ensures it stays visible.
|
|
441
|
+
this.scrollToTab(this.activeTab)
|
|
442
|
+
if (this.activeTab) {
|
|
443
|
+
this.updateFocusableTab(this.activeTab)
|
|
444
|
+
}
|
|
462
445
|
}
|
|
463
446
|
|
|
464
447
|
override render(): TemplateResult {
|
|
465
|
-
const classes: ClassInfo = {
|
|
466
|
-
surface: true,
|
|
467
|
-
scrollable: !!this.scrollable,
|
|
468
|
-
}
|
|
469
448
|
return html`
|
|
470
|
-
<div class="
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
${this.rightScrollControl()} ${this.renderIndicator()}
|
|
474
|
-
<div class="divider"></div>
|
|
475
|
-
</div>
|
|
449
|
+
<div class="tabs">${this.renderSlot()}</div>
|
|
450
|
+
${this.renderIndicator()}
|
|
451
|
+
<ui-divider class="divider"></ui-divider>
|
|
476
452
|
`
|
|
477
453
|
}
|
|
478
454
|
|
|
479
455
|
protected renderSlot(): TemplateResult {
|
|
480
|
-
return html`<slot
|
|
481
|
-
@slotchange="${this.updateItems}"
|
|
482
|
-
@click="${this.contentClickHandler}"
|
|
483
|
-
@keydown="${this.contentKeyDownHandler}"
|
|
484
|
-
></slot>`
|
|
456
|
+
return html`<slot @slotchange="${this.handleSlotChange}" @click="${this.handleTabClick}"></slot>`
|
|
485
457
|
}
|
|
486
458
|
|
|
487
459
|
protected renderIndicator(): TemplateResult {
|
|
@@ -497,26 +469,8 @@ export default class UiTabs extends LitElement {
|
|
|
497
469
|
</div>
|
|
498
470
|
`
|
|
499
471
|
}
|
|
472
|
+
}
|
|
500
473
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return nothing
|
|
504
|
-
}
|
|
505
|
-
return html` <div class="scroll-control left">
|
|
506
|
-
<ui-icon-button aria-label="Scroll tabs left" title="Scroll tabs left" @click="${this.handleScrollLeft}">
|
|
507
|
-
<ui-icon icon="chevronLeft"></ui-icon>
|
|
508
|
-
</ui-icon-button>
|
|
509
|
-
</div>`
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
protected rightScrollControl(): TemplateResult | typeof nothing {
|
|
513
|
-
if (!this.scrollable) {
|
|
514
|
-
return nothing
|
|
515
|
-
}
|
|
516
|
-
return html`<div class="scroll-control right">
|
|
517
|
-
<ui-icon-button aria-label="Scroll tabs right" title="Scroll tabs right" @click="${this.handleScrollRight}">
|
|
518
|
-
<ui-icon icon="chevronRight"></ui-icon>
|
|
519
|
-
</ui-icon-button>
|
|
520
|
-
</div>`
|
|
521
|
-
}
|
|
474
|
+
function isTab(element: unknown): element is UiTab {
|
|
475
|
+
return element instanceof UiTab
|
|
522
476
|
}
|