@vaadin/tabs 24.3.0-alpha1 → 24.3.0-alpha11
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 +10 -9
- package/src/vaadin-tab-mixin.d.ts +18 -0
- package/src/vaadin-tab-mixin.js +40 -0
- package/src/vaadin-tab-styles.d.ts +8 -0
- package/src/vaadin-tab-styles.js +27 -0
- package/src/vaadin-tab.js +7 -44
- package/src/vaadin-tabs-mixin.d.ts +33 -0
- package/src/vaadin-tabs-mixin.js +227 -0
- package/src/vaadin-tabs-styles.d.ts +8 -0
- package/src/vaadin-tabs-styles.js +84 -0
- package/src/vaadin-tabs.d.ts +3 -14
- package/src/vaadin-tabs.js +7 -293
- package/theme/lumo/vaadin-tab-styles.js +9 -5
- package/web-types.json +1 -1
- package/web-types.lit.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/tabs",
|
|
3
|
-
"version": "24.3.0-
|
|
3
|
+
"version": "24.3.0-alpha11",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -35,22 +35,23 @@
|
|
|
35
35
|
"polymer"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@open-wc/dedupe-mixin": "^1.3.0",
|
|
38
39
|
"@polymer/polymer": "^3.0.0",
|
|
39
|
-
"@vaadin/a11y-base": "24.3.0-
|
|
40
|
-
"@vaadin/component-base": "24.3.0-
|
|
41
|
-
"@vaadin/item": "24.3.0-
|
|
42
|
-
"@vaadin/vaadin-lumo-styles": "24.3.0-
|
|
43
|
-
"@vaadin/vaadin-material-styles": "24.3.0-
|
|
44
|
-
"@vaadin/vaadin-themable-mixin": "24.3.0-
|
|
40
|
+
"@vaadin/a11y-base": "24.3.0-alpha11",
|
|
41
|
+
"@vaadin/component-base": "24.3.0-alpha11",
|
|
42
|
+
"@vaadin/item": "24.3.0-alpha11",
|
|
43
|
+
"@vaadin/vaadin-lumo-styles": "24.3.0-alpha11",
|
|
44
|
+
"@vaadin/vaadin-material-styles": "24.3.0-alpha11",
|
|
45
|
+
"@vaadin/vaadin-themable-mixin": "24.3.0-alpha11"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@esm-bundle/chai": "^4.3.4",
|
|
48
|
-
"@vaadin/testing-helpers": "^0.
|
|
49
|
+
"@vaadin/testing-helpers": "^0.6.0",
|
|
49
50
|
"sinon": "^13.0.2"
|
|
50
51
|
},
|
|
51
52
|
"web-types": [
|
|
52
53
|
"web-types.json",
|
|
53
54
|
"web-types.lit.json"
|
|
54
55
|
],
|
|
55
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "123cf569a1b6ef6f4ef5fe8e60cb8d988699b98c"
|
|
56
57
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import type { ActiveMixinClass } from '@vaadin/a11y-base/src/active-mixin.js';
|
|
8
|
+
import type { DisabledMixinClass } from '@vaadin/a11y-base/src/disabled-mixin.js';
|
|
9
|
+
import type { FocusMixinClass } from '@vaadin/a11y-base/src/focus-mixin.js';
|
|
10
|
+
import type { ItemMixinClass } from '@vaadin/item/src/vaadin-item-mixin.js';
|
|
11
|
+
|
|
12
|
+
export declare function TabMixin<T extends Constructor<HTMLElement>>(
|
|
13
|
+
base: T,
|
|
14
|
+
): Constructor<ActiveMixinClass> &
|
|
15
|
+
Constructor<DisabledMixinClass> &
|
|
16
|
+
Constructor<FocusMixinClass> &
|
|
17
|
+
Constructor<ItemMixinClass> &
|
|
18
|
+
T;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { ItemMixin } from '@vaadin/item/src/vaadin-item-mixin.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @polymerMixin
|
|
10
|
+
* @mixes ItemMixin
|
|
11
|
+
*/
|
|
12
|
+
export const TabMixin = (superClass) =>
|
|
13
|
+
class TabMixinClass extends ItemMixin(superClass) {
|
|
14
|
+
/** @protected */
|
|
15
|
+
ready() {
|
|
16
|
+
super.ready();
|
|
17
|
+
|
|
18
|
+
this.setAttribute('role', 'tab');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Override an event listener from `KeyboardMixin`
|
|
23
|
+
* to handle clicking anchors inside the tabs.
|
|
24
|
+
* @param {!KeyboardEvent} event
|
|
25
|
+
* @protected
|
|
26
|
+
* @override
|
|
27
|
+
*/
|
|
28
|
+
_onKeyUp(event) {
|
|
29
|
+
const willClick = this.hasAttribute('active');
|
|
30
|
+
|
|
31
|
+
super._onKeyUp(event);
|
|
32
|
+
|
|
33
|
+
if (willClick) {
|
|
34
|
+
const anchor = this.querySelector('a');
|
|
35
|
+
if (anchor) {
|
|
36
|
+
anchor.click();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { css } from 'lit';
|
|
7
|
+
|
|
8
|
+
export const tabStyles = css`
|
|
9
|
+
:host {
|
|
10
|
+
display: block;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:host([hidden]) {
|
|
14
|
+
display: none !important;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (forced-colors: active) {
|
|
18
|
+
:host([focused]) {
|
|
19
|
+
outline: 1px solid;
|
|
20
|
+
outline-offset: -1px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host([selected]) {
|
|
24
|
+
border-bottom: 2px solid;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
package/src/vaadin-tab.js
CHANGED
|
@@ -8,8 +8,11 @@ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js'
|
|
|
8
8
|
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
9
9
|
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
10
10
|
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
12
|
+
import { TabMixin } from './vaadin-tab-mixin.js';
|
|
13
|
+
import { tabStyles } from './vaadin-tab-styles.js';
|
|
14
|
+
|
|
15
|
+
registerStyles('vaadin-tab', tabStyles, { moduleId: 'vaadin-tab-styles' });
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* `<vaadin-tab>` is a Web Component providing an accessible and customizable tab.
|
|
@@ -38,30 +41,11 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
|
|
|
38
41
|
* @mixes ControllerMixin
|
|
39
42
|
* @mixes ElementMixin
|
|
40
43
|
* @mixes ThemableMixin
|
|
41
|
-
* @mixes
|
|
44
|
+
* @mixes TabMixin
|
|
42
45
|
*/
|
|
43
|
-
class Tab extends ElementMixin(ThemableMixin(
|
|
46
|
+
class Tab extends ElementMixin(ThemableMixin(TabMixin(ControllerMixin(PolymerElement)))) {
|
|
44
47
|
static get template() {
|
|
45
48
|
return html`
|
|
46
|
-
<style>
|
|
47
|
-
:host {
|
|
48
|
-
display: block;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
:host([hidden]) {
|
|
52
|
-
display: none !important;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@media (forced-colors: active) {
|
|
56
|
-
:host([focused]) {
|
|
57
|
-
outline: 1px solid;
|
|
58
|
-
outline-offset: -1px;
|
|
59
|
-
}
|
|
60
|
-
:host([selected]) {
|
|
61
|
-
border-bottom: 2px solid;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
</style>
|
|
65
49
|
<slot></slot>
|
|
66
50
|
<slot name="tooltip"></slot>
|
|
67
51
|
`;
|
|
@@ -74,31 +58,10 @@ class Tab extends ElementMixin(ThemableMixin(ItemMixin(ControllerMixin(PolymerEl
|
|
|
74
58
|
/** @protected */
|
|
75
59
|
ready() {
|
|
76
60
|
super.ready();
|
|
77
|
-
this.setAttribute('role', 'tab');
|
|
78
61
|
|
|
79
62
|
this._tooltipController = new TooltipController(this);
|
|
80
63
|
this.addController(this._tooltipController);
|
|
81
64
|
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Override an event listener from `KeyboardMixin`
|
|
85
|
-
* to handle clicking anchors inside the tabs.
|
|
86
|
-
* @param {!KeyboardEvent} event
|
|
87
|
-
* @protected
|
|
88
|
-
* @override
|
|
89
|
-
*/
|
|
90
|
-
_onKeyUp(event) {
|
|
91
|
-
const willClick = this.hasAttribute('active');
|
|
92
|
-
|
|
93
|
-
super._onKeyUp(event);
|
|
94
|
-
|
|
95
|
-
if (willClick) {
|
|
96
|
-
const anchor = this.querySelector('a');
|
|
97
|
-
if (anchor) {
|
|
98
|
-
anchor.click();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
65
|
}
|
|
103
66
|
|
|
104
67
|
defineCustomElement(Tab);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import type { KeyboardDirectionMixinClass } from '@vaadin/a11y-base/src/keyboard-direction-mixin.js';
|
|
8
|
+
import type { KeyboardMixinClass } from '@vaadin/a11y-base/src/keyboard-mixin.js';
|
|
9
|
+
import type { ListMixinClass } from '@vaadin/a11y-base/src/list-mixin.js';
|
|
10
|
+
import type { ResizeMixinClass } from '@vaadin/component-base/src/resize-mixin.js';
|
|
11
|
+
|
|
12
|
+
export type TabsOrientation = 'horizontal' | 'vertical';
|
|
13
|
+
|
|
14
|
+
export declare function TabsMixin<T extends Constructor<HTMLElement>>(
|
|
15
|
+
base: T,
|
|
16
|
+
): Constructor<KeyboardDirectionMixinClass> &
|
|
17
|
+
Constructor<KeyboardMixinClass> &
|
|
18
|
+
Constructor<ListMixinClass> &
|
|
19
|
+
Constructor<ResizeMixinClass> &
|
|
20
|
+
Constructor<TabsMixinClass> &
|
|
21
|
+
T;
|
|
22
|
+
|
|
23
|
+
export declare class TabsMixinClass {
|
|
24
|
+
/**
|
|
25
|
+
* The index of the selected tab.
|
|
26
|
+
*/
|
|
27
|
+
selected: number | null | undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Set tabs disposition. Possible values are `horizontal|vertical`
|
|
31
|
+
*/
|
|
32
|
+
orientation: TabsOrientation;
|
|
33
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
|
7
|
+
import { ListMixin } from '@vaadin/a11y-base/src/list-mixin.js';
|
|
8
|
+
import { getNormalizedScrollLeft } from '@vaadin/component-base/src/dir-utils.js';
|
|
9
|
+
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @polymerMixin
|
|
13
|
+
* @mixes ListMixin
|
|
14
|
+
* @mixes ResizeMixin
|
|
15
|
+
*/
|
|
16
|
+
export const TabsMixin = (superClass) =>
|
|
17
|
+
class TabsMixinClass extends ResizeMixin(ListMixin(superClass)) {
|
|
18
|
+
static get properties() {
|
|
19
|
+
return {
|
|
20
|
+
/**
|
|
21
|
+
* Set tabs disposition. Possible values are `horizontal|vertical`
|
|
22
|
+
* @type {!TabsOrientation}
|
|
23
|
+
*/
|
|
24
|
+
orientation: {
|
|
25
|
+
value: 'horizontal',
|
|
26
|
+
type: String,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The index of the selected tab.
|
|
31
|
+
*/
|
|
32
|
+
selected: {
|
|
33
|
+
value: 0,
|
|
34
|
+
type: Number,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static get observers() {
|
|
40
|
+
return ['__tabsItemsChanged(items)'];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
|
|
46
|
+
this.__itemsResizeObserver = new ResizeObserver(() => {
|
|
47
|
+
setTimeout(() => this._updateOverflow());
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @return {number}
|
|
53
|
+
* @protected
|
|
54
|
+
*/
|
|
55
|
+
get _scrollOffset() {
|
|
56
|
+
return this._vertical ? this._scrollerElement.offsetHeight : this._scrollerElement.offsetWidth;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @return {!HTMLElement}
|
|
61
|
+
* @protected
|
|
62
|
+
* @override
|
|
63
|
+
*/
|
|
64
|
+
get _scrollerElement() {
|
|
65
|
+
return this.$.scroll;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @private */
|
|
69
|
+
get __direction() {
|
|
70
|
+
return !this._vertical && this.__isRTL ? 1 : -1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @protected */
|
|
74
|
+
ready() {
|
|
75
|
+
super.ready();
|
|
76
|
+
|
|
77
|
+
this._scrollerElement.addEventListener('scroll', () => this._updateOverflow());
|
|
78
|
+
|
|
79
|
+
this.setAttribute('role', 'tablist');
|
|
80
|
+
|
|
81
|
+
// Wait for the vaadin-tab elements to upgrade and get styled
|
|
82
|
+
afterNextRender(this, () => {
|
|
83
|
+
this._updateOverflow();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @protected
|
|
89
|
+
* @override
|
|
90
|
+
*/
|
|
91
|
+
_onResize() {
|
|
92
|
+
this._updateOverflow();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @private */
|
|
96
|
+
__tabsItemsChanged(items) {
|
|
97
|
+
// Disconnected to unobserve any removed items
|
|
98
|
+
this.__itemsResizeObserver.disconnect();
|
|
99
|
+
|
|
100
|
+
// Observe current items
|
|
101
|
+
(items || []).forEach((item) => {
|
|
102
|
+
this.__itemsResizeObserver.observe(item);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
this._updateOverflow();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @protected */
|
|
109
|
+
_scrollForward() {
|
|
110
|
+
// Calculations here are performed in order to optimize the loop that checks item visibility.
|
|
111
|
+
const forwardButtonVisibleWidth = this._getNavigationButtonVisibleWidth('forward-button');
|
|
112
|
+
const backButtonVisibleWidth = this._getNavigationButtonVisibleWidth('back-button');
|
|
113
|
+
const scrollerRect = this._scrollerElement.getBoundingClientRect();
|
|
114
|
+
const itemToScrollTo = [...this.items]
|
|
115
|
+
.reverse()
|
|
116
|
+
.find((item) => this._isItemVisible(item, forwardButtonVisibleWidth, backButtonVisibleWidth, scrollerRect));
|
|
117
|
+
const itemRect = itemToScrollTo.getBoundingClientRect();
|
|
118
|
+
// This hard-coded number accounts for the width of the mask that covers a part of the visible items.
|
|
119
|
+
// A CSS variable can be introduced to get rid of this value.
|
|
120
|
+
const overflowIndicatorCompensation = 20;
|
|
121
|
+
const totalCompensation =
|
|
122
|
+
overflowIndicatorCompensation + this.shadowRoot.querySelector('[part="back-button"]').clientWidth;
|
|
123
|
+
let scrollOffset;
|
|
124
|
+
if (this.__isRTL) {
|
|
125
|
+
const scrollerRightEdge = scrollerRect.right - totalCompensation;
|
|
126
|
+
scrollOffset = itemRect.right - scrollerRightEdge;
|
|
127
|
+
} else {
|
|
128
|
+
const scrollerLeftEdge = scrollerRect.left + totalCompensation;
|
|
129
|
+
scrollOffset = itemRect.left - scrollerLeftEdge;
|
|
130
|
+
}
|
|
131
|
+
// It is possible that a scroll offset is calculated to be between 0 and 1. In this case, this offset
|
|
132
|
+
// can be rounded down to zero, rendering the button useless. It is also possible that the offset is
|
|
133
|
+
// calculated such that it results in scrolling backwards for a wide tab or edge cases. This is a
|
|
134
|
+
// workaround for such cases.
|
|
135
|
+
if (-this.__direction * scrollOffset < 1) {
|
|
136
|
+
scrollOffset = -this.__direction * (this._scrollOffset - totalCompensation);
|
|
137
|
+
}
|
|
138
|
+
this._scroll(scrollOffset);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @protected */
|
|
142
|
+
_scrollBack() {
|
|
143
|
+
// Calculations here are performed in order to optimize the loop that checks item visibility.
|
|
144
|
+
const forwardButtonVisibleWidth = this._getNavigationButtonVisibleWidth('forward-button');
|
|
145
|
+
const backButtonVisibleWidth = this._getNavigationButtonVisibleWidth('back-button');
|
|
146
|
+
const scrollerRect = this._scrollerElement.getBoundingClientRect();
|
|
147
|
+
const itemToScrollTo = this.items.find((item) =>
|
|
148
|
+
this._isItemVisible(item, forwardButtonVisibleWidth, backButtonVisibleWidth, scrollerRect),
|
|
149
|
+
);
|
|
150
|
+
const itemRect = itemToScrollTo.getBoundingClientRect();
|
|
151
|
+
// This hard-coded number accounts for the width of the mask that covers a part of the visible items.
|
|
152
|
+
// A CSS variable can be introduced to get rid of this value.
|
|
153
|
+
const overflowIndicatorCompensation = 20;
|
|
154
|
+
const totalCompensation =
|
|
155
|
+
overflowIndicatorCompensation + this.shadowRoot.querySelector('[part="forward-button"]').clientWidth;
|
|
156
|
+
let scrollOffset;
|
|
157
|
+
if (this.__isRTL) {
|
|
158
|
+
const scrollerLeftEdge = scrollerRect.left + totalCompensation;
|
|
159
|
+
scrollOffset = itemRect.left - scrollerLeftEdge;
|
|
160
|
+
} else {
|
|
161
|
+
const scrollerRightEdge = scrollerRect.right - totalCompensation;
|
|
162
|
+
scrollOffset = itemRect.right - scrollerRightEdge;
|
|
163
|
+
}
|
|
164
|
+
// It is possible that a scroll offset is calculated to be between 0 and 1. In this case, this offset
|
|
165
|
+
// can be rounded down to zero, rendering the button useless. It is also possible that the offset is
|
|
166
|
+
// calculated such that it results in scrolling forward for a wide tab or edge cases. This is a
|
|
167
|
+
// workaround for such cases.
|
|
168
|
+
if (this.__direction * scrollOffset < 1) {
|
|
169
|
+
scrollOffset = this.__direction * (this._scrollOffset - totalCompensation);
|
|
170
|
+
}
|
|
171
|
+
this._scroll(scrollOffset);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @private */
|
|
175
|
+
_isItemVisible(item, forwardButtonVisibleWidth, backButtonVisibleWidth, scrollerRect) {
|
|
176
|
+
if (this._vertical) {
|
|
177
|
+
throw new Error('Visibility check is only supported for horizontal tabs.');
|
|
178
|
+
}
|
|
179
|
+
const buttonOnTheRightWidth = this.__isRTL ? backButtonVisibleWidth : forwardButtonVisibleWidth;
|
|
180
|
+
const buttonOnTheLeftWidth = this.__isRTL ? forwardButtonVisibleWidth : backButtonVisibleWidth;
|
|
181
|
+
const scrollerRightEdge = scrollerRect.right - buttonOnTheRightWidth;
|
|
182
|
+
const scrollerLeftEdge = scrollerRect.left + buttonOnTheLeftWidth;
|
|
183
|
+
const itemRect = item.getBoundingClientRect();
|
|
184
|
+
return scrollerRightEdge > Math.floor(itemRect.left) && scrollerLeftEdge < Math.ceil(itemRect.right);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @private */
|
|
188
|
+
_getNavigationButtonVisibleWidth(buttonPartName) {
|
|
189
|
+
const navigationButton = this.shadowRoot.querySelector(`[part="${buttonPartName}"]`);
|
|
190
|
+
if (window.getComputedStyle(navigationButton).opacity === '0') {
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
return navigationButton.clientWidth;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @private */
|
|
197
|
+
_updateOverflow() {
|
|
198
|
+
const scrollPosition = this._vertical
|
|
199
|
+
? this._scrollerElement.scrollTop
|
|
200
|
+
: getNormalizedScrollLeft(this._scrollerElement, this.getAttribute('dir'));
|
|
201
|
+
const scrollSize = this._vertical ? this._scrollerElement.scrollHeight : this._scrollerElement.scrollWidth;
|
|
202
|
+
|
|
203
|
+
// Note that we are not comparing floored scrollPosition to be greater than zero here, which would make a natural
|
|
204
|
+
// sense, but to be greater than one intentionally. There is a known bug in Chromium browsers on Linux/Mac
|
|
205
|
+
// (https://bugs.chromium.org/p/chromium/issues/detail?id=1123301), which returns invalid value of scrollLeft when
|
|
206
|
+
// text direction is RTL. The value is off by one pixel in that case. Comparing scrollPosition to be greater than
|
|
207
|
+
// one on the following line is a workaround for that bug. Comparing scrollPosition to be greater than one means,
|
|
208
|
+
// that the left overflow and left arrow will be displayed "one pixel" later than normal. In other words, if the tab
|
|
209
|
+
// scroller element is scrolled just by 1px, the overflow is not yet showing.
|
|
210
|
+
let overflow = Math.floor(scrollPosition) > 1 ? 'start' : '';
|
|
211
|
+
if (Math.ceil(scrollPosition) < Math.ceil(scrollSize - this._scrollOffset)) {
|
|
212
|
+
overflow += ' end';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (this.__direction === 1) {
|
|
216
|
+
overflow = overflow.replace(/start|end/giu, (matched) => {
|
|
217
|
+
return matched === 'start' ? 'end' : 'start';
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (overflow) {
|
|
222
|
+
this.setAttribute('overflow', overflow.trim());
|
|
223
|
+
} else {
|
|
224
|
+
this.removeAttribute('overflow');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { css } from 'lit';
|
|
7
|
+
|
|
8
|
+
export const tabsStyles = css`
|
|
9
|
+
:host {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:host([hidden]) {
|
|
15
|
+
display: none !important;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:host([orientation='vertical']) {
|
|
19
|
+
display: block;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:host([orientation='horizontal']) [part='tabs'] {
|
|
23
|
+
flex-grow: 1;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-self: stretch;
|
|
26
|
+
overflow-x: auto;
|
|
27
|
+
-webkit-overflow-scrolling: touch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* This seems more future-proof than \`overflow: -moz-scrollbars-none\` which is marked obsolete
|
|
31
|
+
and is no longer guaranteed to work:
|
|
32
|
+
https://developer.mozilla.org/en-US/docs/Web/CSS/overflow#Mozilla_Extensions */
|
|
33
|
+
@-moz-document url-prefix() {
|
|
34
|
+
:host([orientation='horizontal']) [part='tabs'] {
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:host([orientation='horizontal']) [part='tabs']::-webkit-scrollbar {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:host([orientation='vertical']) [part='tabs'] {
|
|
44
|
+
height: 100%;
|
|
45
|
+
overflow-y: auto;
|
|
46
|
+
-webkit-overflow-scrolling: touch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[part='back-button'],
|
|
50
|
+
[part='forward-button'] {
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
opacity: 0;
|
|
53
|
+
cursor: default;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:host([overflow~='start']) [part='back-button'],
|
|
57
|
+
:host([overflow~='end']) [part='forward-button'] {
|
|
58
|
+
pointer-events: auto;
|
|
59
|
+
opacity: 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[part='back-button']::after {
|
|
63
|
+
content: '\\25C0';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[part='forward-button']::after {
|
|
67
|
+
content: '\\25B6';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:host([orientation='vertical']) [part='back-button'],
|
|
71
|
+
:host([orientation='vertical']) [part='forward-button'] {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* RTL specific styles */
|
|
76
|
+
|
|
77
|
+
:host([dir='rtl']) [part='back-button']::after {
|
|
78
|
+
content: '\\25B6';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:host([dir='rtl']) [part='forward-button']::after {
|
|
82
|
+
content: '\\25C0';
|
|
83
|
+
}
|
|
84
|
+
`;
|
package/src/vaadin-tabs.d.ts
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* Copyright (c) 2017 - 2023 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import { ListMixin } from '@vaadin/a11y-base/src/list-mixin.js';
|
|
7
6
|
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
8
|
-
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
|
|
9
7
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
8
|
+
import { TabsMixin, TabsOrientation } from './vaadin-tabs-mixin.js';
|
|
10
9
|
|
|
11
|
-
export
|
|
10
|
+
export { TabsOrientation };
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Fired when the `items` property changes.
|
|
@@ -62,17 +61,7 @@ export interface TabsEventMap extends HTMLElementEventMap, TabsCustomEventMap {}
|
|
|
62
61
|
* @fires {CustomEvent} items-changed - Fired when the `items` property changes.
|
|
63
62
|
* @fires {CustomEvent} selected-changed - Fired when the `selected` property changes.
|
|
64
63
|
*/
|
|
65
|
-
declare class Tabs extends
|
|
66
|
-
/**
|
|
67
|
-
* The index of the selected tab.
|
|
68
|
-
*/
|
|
69
|
-
selected: number | null | undefined;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Set tabs disposition. Possible values are `horizontal|vertical`
|
|
73
|
-
*/
|
|
74
|
-
orientation: TabsOrientation;
|
|
75
|
-
|
|
64
|
+
declare class Tabs extends TabsMixin(ElementMixin(ThemableMixin(HTMLElement))) {
|
|
76
65
|
addEventListener<K extends keyof TabsEventMap>(
|
|
77
66
|
type: K,
|
|
78
67
|
listener: (this: Tabs, ev: TabsEventMap[K]) => void,
|
package/src/vaadin-tabs.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import './vaadin-tab.js';
|
|
7
|
-
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
|
8
7
|
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
9
|
-
import { ListMixin } from '@vaadin/a11y-base/src/list-mixin.js';
|
|
10
8
|
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
11
|
-
import { getNormalizedScrollLeft } from '@vaadin/component-base/src/dir-utils.js';
|
|
12
9
|
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
10
|
+
import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
11
|
+
import { TabsMixin } from './vaadin-tabs-mixin.js';
|
|
12
|
+
import { tabsStyles } from './vaadin-tabs-styles.js';
|
|
13
|
+
|
|
14
|
+
registerStyles('vaadin-tabs', tabsStyles, { moduleId: 'vaadin-tabs-styles' });
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* `<vaadin-tabs>` is a Web Component for organizing and grouping content into sections.
|
|
@@ -50,90 +50,12 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
|
|
|
50
50
|
* @customElement
|
|
51
51
|
* @extends HTMLElement
|
|
52
52
|
* @mixes ElementMixin
|
|
53
|
-
* @mixes
|
|
53
|
+
* @mixes TabsMixin
|
|
54
54
|
* @mixes ThemableMixin
|
|
55
|
-
* @mixes ResizeMixin
|
|
56
55
|
*/
|
|
57
|
-
class Tabs extends
|
|
56
|
+
class Tabs extends TabsMixin(ElementMixin(ThemableMixin(PolymerElement))) {
|
|
58
57
|
static get template() {
|
|
59
58
|
return html`
|
|
60
|
-
<style>
|
|
61
|
-
:host {
|
|
62
|
-
display: flex;
|
|
63
|
-
align-items: center;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
:host([hidden]) {
|
|
67
|
-
display: none !important;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
:host([orientation='vertical']) {
|
|
71
|
-
display: block;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
:host([orientation='horizontal']) [part='tabs'] {
|
|
75
|
-
flex-grow: 1;
|
|
76
|
-
display: flex;
|
|
77
|
-
align-self: stretch;
|
|
78
|
-
overflow-x: auto;
|
|
79
|
-
-webkit-overflow-scrolling: touch;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/* This seems more future-proof than \`overflow: -moz-scrollbars-none\` which is marked obsolete
|
|
83
|
-
and is no longer guaranteed to work:
|
|
84
|
-
https://developer.mozilla.org/en-US/docs/Web/CSS/overflow#Mozilla_Extensions */
|
|
85
|
-
@-moz-document url-prefix() {
|
|
86
|
-
:host([orientation='horizontal']) [part='tabs'] {
|
|
87
|
-
overflow: hidden;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
:host([orientation='horizontal']) [part='tabs']::-webkit-scrollbar {
|
|
92
|
-
display: none;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
:host([orientation='vertical']) [part='tabs'] {
|
|
96
|
-
height: 100%;
|
|
97
|
-
overflow-y: auto;
|
|
98
|
-
-webkit-overflow-scrolling: touch;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
[part='back-button'],
|
|
102
|
-
[part='forward-button'] {
|
|
103
|
-
pointer-events: none;
|
|
104
|
-
opacity: 0;
|
|
105
|
-
cursor: default;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
:host([overflow~='start']) [part='back-button'],
|
|
109
|
-
:host([overflow~='end']) [part='forward-button'] {
|
|
110
|
-
pointer-events: auto;
|
|
111
|
-
opacity: 1;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
[part='back-button']::after {
|
|
115
|
-
content: '\\25C0';
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
[part='forward-button']::after {
|
|
119
|
-
content: '\\25B6';
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
:host([orientation='vertical']) [part='back-button'],
|
|
123
|
-
:host([orientation='vertical']) [part='forward-button'] {
|
|
124
|
-
display: none;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/* RTL specific styles */
|
|
128
|
-
|
|
129
|
-
:host([dir='rtl']) [part='back-button']::after {
|
|
130
|
-
content: '\\25B6';
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
:host([dir='rtl']) [part='forward-button']::after {
|
|
134
|
-
content: '\\25C0';
|
|
135
|
-
}
|
|
136
|
-
</style>
|
|
137
59
|
<div on-click="_scrollBack" part="back-button" aria-hidden="true"></div>
|
|
138
60
|
|
|
139
61
|
<div id="scroll" part="tabs">
|
|
@@ -147,214 +69,6 @@ class Tabs extends ResizeMixin(ElementMixin(ListMixin(ThemableMixin(PolymerEleme
|
|
|
147
69
|
static get is() {
|
|
148
70
|
return 'vaadin-tabs';
|
|
149
71
|
}
|
|
150
|
-
|
|
151
|
-
static get properties() {
|
|
152
|
-
return {
|
|
153
|
-
/**
|
|
154
|
-
* Set tabs disposition. Possible values are `horizontal|vertical`
|
|
155
|
-
* @type {!TabsOrientation}
|
|
156
|
-
*/
|
|
157
|
-
orientation: {
|
|
158
|
-
value: 'horizontal',
|
|
159
|
-
type: String,
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The index of the selected tab.
|
|
164
|
-
*/
|
|
165
|
-
selected: {
|
|
166
|
-
value: 0,
|
|
167
|
-
type: Number,
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
static get observers() {
|
|
173
|
-
return ['__tabsItemsChanged(items, items.*)'];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
constructor() {
|
|
177
|
-
super();
|
|
178
|
-
|
|
179
|
-
this.__itemsResizeObserver = new ResizeObserver(() => {
|
|
180
|
-
setTimeout(() => this._updateOverflow());
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* @return {number}
|
|
186
|
-
* @protected
|
|
187
|
-
*/
|
|
188
|
-
get _scrollOffset() {
|
|
189
|
-
return this._vertical ? this._scrollerElement.offsetHeight : this._scrollerElement.offsetWidth;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* @return {!HTMLElement}
|
|
194
|
-
* @protected
|
|
195
|
-
* @override
|
|
196
|
-
*/
|
|
197
|
-
get _scrollerElement() {
|
|
198
|
-
return this.$.scroll;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** @private */
|
|
202
|
-
get __direction() {
|
|
203
|
-
return !this._vertical && this.__isRTL ? 1 : -1;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** @protected */
|
|
207
|
-
ready() {
|
|
208
|
-
super.ready();
|
|
209
|
-
this._scrollerElement.addEventListener('scroll', () => this._updateOverflow());
|
|
210
|
-
this.setAttribute('role', 'tablist');
|
|
211
|
-
|
|
212
|
-
// Wait for the vaadin-tab elements to upgrade and get styled
|
|
213
|
-
afterNextRender(this, () => {
|
|
214
|
-
this._updateOverflow();
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* @protected
|
|
220
|
-
* @override
|
|
221
|
-
*/
|
|
222
|
-
_onResize() {
|
|
223
|
-
this._updateOverflow();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** @private */
|
|
227
|
-
__tabsItemsChanged(items) {
|
|
228
|
-
// Disconnected to unobserve any removed items
|
|
229
|
-
this.__itemsResizeObserver.disconnect();
|
|
230
|
-
|
|
231
|
-
// Observe current items
|
|
232
|
-
(items || []).forEach((item) => {
|
|
233
|
-
this.__itemsResizeObserver.observe(item);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
this._updateOverflow();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** @private */
|
|
240
|
-
_scrollForward() {
|
|
241
|
-
// Calculations here are performed in order to optimize the loop that checks item visibility.
|
|
242
|
-
const forwardButtonVisibleWidth = this._getNavigationButtonVisibleWidth('forward-button');
|
|
243
|
-
const backButtonVisibleWidth = this._getNavigationButtonVisibleWidth('back-button');
|
|
244
|
-
const scrollerRect = this._scrollerElement.getBoundingClientRect();
|
|
245
|
-
const itemToScrollTo = [...this.items]
|
|
246
|
-
.reverse()
|
|
247
|
-
.find((item) => this._isItemVisible(item, forwardButtonVisibleWidth, backButtonVisibleWidth, scrollerRect));
|
|
248
|
-
const itemRect = itemToScrollTo.getBoundingClientRect();
|
|
249
|
-
// This hard-coded number accounts for the width of the mask that covers a part of the visible items.
|
|
250
|
-
// A CSS variable can be introduced to get rid of this value.
|
|
251
|
-
const overflowIndicatorCompensation = 20;
|
|
252
|
-
const totalCompensation =
|
|
253
|
-
overflowIndicatorCompensation + this.shadowRoot.querySelector('[part="back-button"]').clientWidth;
|
|
254
|
-
let scrollOffset;
|
|
255
|
-
if (this.__isRTL) {
|
|
256
|
-
const scrollerRightEdge = scrollerRect.right - totalCompensation;
|
|
257
|
-
scrollOffset = itemRect.right - scrollerRightEdge;
|
|
258
|
-
} else {
|
|
259
|
-
const scrollerLeftEdge = scrollerRect.left + totalCompensation;
|
|
260
|
-
scrollOffset = itemRect.left - scrollerLeftEdge;
|
|
261
|
-
}
|
|
262
|
-
// It is possible that a scroll offset is calculated to be between 0 and 1. In this case, this offset
|
|
263
|
-
// can be rounded down to zero, rendering the button useless. It is also possible that the offset is
|
|
264
|
-
// calculated such that it results in scrolling backwards for a wide tab or edge cases. This is a
|
|
265
|
-
// workaround for such cases.
|
|
266
|
-
if (-this.__direction * scrollOffset < 1) {
|
|
267
|
-
scrollOffset = -this.__direction * (this._scrollOffset - totalCompensation);
|
|
268
|
-
}
|
|
269
|
-
this._scroll(scrollOffset);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** @private */
|
|
273
|
-
_scrollBack() {
|
|
274
|
-
// Calculations here are performed in order to optimize the loop that checks item visibility.
|
|
275
|
-
const forwardButtonVisibleWidth = this._getNavigationButtonVisibleWidth('forward-button');
|
|
276
|
-
const backButtonVisibleWidth = this._getNavigationButtonVisibleWidth('back-button');
|
|
277
|
-
const scrollerRect = this._scrollerElement.getBoundingClientRect();
|
|
278
|
-
const itemToScrollTo = this.items.find((item) =>
|
|
279
|
-
this._isItemVisible(item, forwardButtonVisibleWidth, backButtonVisibleWidth, scrollerRect),
|
|
280
|
-
);
|
|
281
|
-
const itemRect = itemToScrollTo.getBoundingClientRect();
|
|
282
|
-
// This hard-coded number accounts for the width of the mask that covers a part of the visible items.
|
|
283
|
-
// A CSS variable can be introduced to get rid of this value.
|
|
284
|
-
const overflowIndicatorCompensation = 20;
|
|
285
|
-
const totalCompensation =
|
|
286
|
-
overflowIndicatorCompensation + this.shadowRoot.querySelector('[part="forward-button"]').clientWidth;
|
|
287
|
-
let scrollOffset;
|
|
288
|
-
if (this.__isRTL) {
|
|
289
|
-
const scrollerLeftEdge = scrollerRect.left + totalCompensation;
|
|
290
|
-
scrollOffset = itemRect.left - scrollerLeftEdge;
|
|
291
|
-
} else {
|
|
292
|
-
const scrollerRightEdge = scrollerRect.right - totalCompensation;
|
|
293
|
-
scrollOffset = itemRect.right - scrollerRightEdge;
|
|
294
|
-
}
|
|
295
|
-
// It is possible that a scroll offset is calculated to be between 0 and 1. In this case, this offset
|
|
296
|
-
// can be rounded down to zero, rendering the button useless. It is also possible that the offset is
|
|
297
|
-
// calculated such that it results in scrolling forward for a wide tab or edge cases. This is a
|
|
298
|
-
// workaround for such cases.
|
|
299
|
-
if (this.__direction * scrollOffset < 1) {
|
|
300
|
-
scrollOffset = this.__direction * (this._scrollOffset - totalCompensation);
|
|
301
|
-
}
|
|
302
|
-
this._scroll(scrollOffset);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** @private */
|
|
306
|
-
_isItemVisible(item, forwardButtonVisibleWidth, backButtonVisibleWidth, scrollerRect) {
|
|
307
|
-
if (this._vertical) {
|
|
308
|
-
throw new Error('Visibility check is only supported for horizontal tabs.');
|
|
309
|
-
}
|
|
310
|
-
const buttonOnTheRightWidth = this.__isRTL ? backButtonVisibleWidth : forwardButtonVisibleWidth;
|
|
311
|
-
const buttonOnTheLeftWidth = this.__isRTL ? forwardButtonVisibleWidth : backButtonVisibleWidth;
|
|
312
|
-
const scrollerRightEdge = scrollerRect.right - buttonOnTheRightWidth;
|
|
313
|
-
const scrollerLeftEdge = scrollerRect.left + buttonOnTheLeftWidth;
|
|
314
|
-
const itemRect = item.getBoundingClientRect();
|
|
315
|
-
return scrollerRightEdge > Math.floor(itemRect.left) && scrollerLeftEdge < Math.ceil(itemRect.right);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** @private */
|
|
319
|
-
_getNavigationButtonVisibleWidth(buttonPartName) {
|
|
320
|
-
const navigationButton = this.shadowRoot.querySelector(`[part="${buttonPartName}"]`);
|
|
321
|
-
if (window.getComputedStyle(navigationButton).opacity === '0') {
|
|
322
|
-
return 0;
|
|
323
|
-
}
|
|
324
|
-
return navigationButton.clientWidth;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** @private */
|
|
328
|
-
_updateOverflow() {
|
|
329
|
-
const scrollPosition = this._vertical
|
|
330
|
-
? this._scrollerElement.scrollTop
|
|
331
|
-
: getNormalizedScrollLeft(this._scrollerElement, this.getAttribute('dir'));
|
|
332
|
-
const scrollSize = this._vertical ? this._scrollerElement.scrollHeight : this._scrollerElement.scrollWidth;
|
|
333
|
-
|
|
334
|
-
// Note that we are not comparing floored scrollPosition to be greater than zero here, which would make a natural
|
|
335
|
-
// sense, but to be greater than one intentionally. There is a known bug in Chromium browsers on Linux/Mac
|
|
336
|
-
// (https://bugs.chromium.org/p/chromium/issues/detail?id=1123301), which returns invalid value of scrollLeft when
|
|
337
|
-
// text direction is RTL. The value is off by one pixel in that case. Comparing scrollPosition to be greater than
|
|
338
|
-
// one on the following line is a workaround for that bug. Comparing scrollPosition to be greater than one means,
|
|
339
|
-
// that the left overflow and left arrow will be displayed "one pixel" later than normal. In other words, if the tab
|
|
340
|
-
// scroller element is scrolled just by 1px, the overflow is not yet showing.
|
|
341
|
-
let overflow = Math.floor(scrollPosition) > 1 ? 'start' : '';
|
|
342
|
-
if (Math.ceil(scrollPosition) < Math.ceil(scrollSize - this._scrollOffset)) {
|
|
343
|
-
overflow += ' end';
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (this.__direction === 1) {
|
|
347
|
-
overflow = overflow.replace(/start|end/giu, (matched) => {
|
|
348
|
-
return matched === 'start' ? 'end' : 'start';
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (overflow) {
|
|
353
|
-
this.setAttribute('overflow', overflow.trim());
|
|
354
|
-
} else {
|
|
355
|
-
this.removeAttribute('overflow');
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
72
|
}
|
|
359
73
|
|
|
360
74
|
defineCustomElement(Tabs);
|
|
@@ -31,6 +31,10 @@ registerStyles(
|
|
|
31
31
|
-webkit-user-select: none;
|
|
32
32
|
-moz-user-select: none;
|
|
33
33
|
user-select: none;
|
|
34
|
+
--_focus-ring-color: var(--vaadin-focus-ring-color, var(--lumo-primary-color-50pct));
|
|
35
|
+
--_focus-ring-width: var(--vaadin-focus-ring-width, 2px);
|
|
36
|
+
--_selection-color: var(--vaadin-selection-color, var(--lumo-primary-color));
|
|
37
|
+
--_selection-color-text: var(--vaadin-selection-color-text, var(--lumo-primary-text-color));
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
:host(:not([orientation='vertical'])) {
|
|
@@ -62,12 +66,12 @@ registerStyles(
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
:host([selected]) {
|
|
65
|
-
color: var(--
|
|
69
|
+
color: var(--_selection-color-text);
|
|
66
70
|
transition: 0.6s color;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
:host([active]:not([selected])) {
|
|
70
|
-
color: var(--
|
|
74
|
+
color: var(--_selection-color-text);
|
|
71
75
|
transition-duration: 0.1s;
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -90,7 +94,7 @@ registerStyles(
|
|
|
90
94
|
|
|
91
95
|
:host([selected])::before,
|
|
92
96
|
:host([selected])::after {
|
|
93
|
-
background-color: var(--
|
|
97
|
+
background-color: var(--_selection-color);
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
:host([orientation='vertical'])::before,
|
|
@@ -105,7 +109,7 @@ registerStyles(
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
:host::after {
|
|
108
|
-
box-shadow: 0 0 0 4px var(--
|
|
112
|
+
box-shadow: 0 0 0 4px var(--_selection-color);
|
|
109
113
|
opacity: 0.15;
|
|
110
114
|
transition: 0.15s 0.02s transform, 0.8s 0.17s opacity;
|
|
111
115
|
}
|
|
@@ -198,7 +202,7 @@ registerStyles(
|
|
|
198
202
|
/* Focus-ring */
|
|
199
203
|
|
|
200
204
|
:host([focus-ring]) {
|
|
201
|
-
box-shadow: inset 0 0 0
|
|
205
|
+
box-shadow: inset 0 0 0 var(--_focus-ring-width) var(--_focus-ring-color);
|
|
202
206
|
border-radius: var(--lumo-border-radius-m);
|
|
203
207
|
}
|
|
204
208
|
|
package/web-types.json
CHANGED