@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/tabs",
3
- "version": "24.3.0-alpha1",
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-alpha1",
40
- "@vaadin/component-base": "24.3.0-alpha1",
41
- "@vaadin/item": "24.3.0-alpha1",
42
- "@vaadin/vaadin-lumo-styles": "24.3.0-alpha1",
43
- "@vaadin/vaadin-material-styles": "24.3.0-alpha1",
44
- "@vaadin/vaadin-themable-mixin": "24.3.0-alpha1"
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.5.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": "9ca6f3ca220a777e8eea181a1f5717e39a732240"
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,8 @@
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 { CSSResult } from 'lit';
7
+
8
+ export const tabStyles: CSSResult;
@@ -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 { ItemMixin } from '@vaadin/item/src/vaadin-item-mixin.js';
12
- import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
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 ItemMixin
44
+ * @mixes TabMixin
42
45
  */
43
- class Tab extends ElementMixin(ThemableMixin(ItemMixin(ControllerMixin(PolymerElement)))) {
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,8 @@
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 { CSSResult } from 'lit';
7
+
8
+ export const tabsStyles: CSSResult;
@@ -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
+ `;
@@ -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 type TabsOrientation = 'horizontal' | 'vertical';
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 ResizeMixin(ElementMixin(ListMixin(ThemableMixin(HTMLElement)))) {
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,
@@ -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 { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
14
- import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
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 ListMixin
53
+ * @mixes TabsMixin
54
54
  * @mixes ThemableMixin
55
- * @mixes ResizeMixin
56
55
  */
57
- class Tabs extends ResizeMixin(ElementMixin(ListMixin(ThemableMixin(PolymerElement)))) {
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(--lumo-primary-text-color);
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(--lumo-primary-text-color);
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(--lumo-primary-color);
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(--lumo-primary-color);
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 2px var(--lumo-primary-color-50pct);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/tabs",
4
- "version": "24.3.0-alpha1",
4
+ "version": "24.3.0-alpha11",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/tabs",
4
- "version": "24.3.0-alpha1",
4
+ "version": "24.3.0-alpha11",
5
5
  "description-markup": "markdown",
6
6
  "framework": "lit",
7
7
  "framework-config": {