diff --git a/packages/tabsheet/package.json b/packages/tabsheet/package.json index d13e47c37b..feda50db84 100644 --- a/packages/tabsheet/package.json +++ b/packages/tabsheet/package.json @@ -21,6 +21,9 @@ "type": "module", "files": [ "src", + "!src/vaadin-lit-tabsheet-scroller.js", + "!src/vaadin-lit-tabsheet.d.ts", + "!src/vaadin-lit-tabsheet.js", "theme", "vaadin-*.d.ts", "vaadin-*.js", @@ -42,7 +45,8 @@ "@vaadin/tabs": "24.6.0-alpha4", "@vaadin/vaadin-lumo-styles": "24.6.0-alpha4", "@vaadin/vaadin-material-styles": "24.6.0-alpha4", - "@vaadin/vaadin-themable-mixin": "24.6.0-alpha4" + "@vaadin/vaadin-themable-mixin": "24.6.0-alpha4", + "lit": "^3.0.0" }, "devDependencies": { "@vaadin/chai-plugins": "24.6.0-alpha4", diff --git a/packages/tabsheet/src/vaadin-lit-tabsheet-scroller.js b/packages/tabsheet/src/vaadin-lit-tabsheet-scroller.js new file mode 100644 index 0000000000..a52347d693 --- /dev/null +++ b/packages/tabsheet/src/vaadin-lit-tabsheet-scroller.js @@ -0,0 +1,21 @@ +/** + * @license + * Copyright (c) 2022 - 2024 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { defineCustomElement } from '@vaadin/component-base/src/define.js'; +import { Scroller } from '@vaadin/scroller/src/vaadin-lit-scroller.js'; + +/** + * An element used internally by ``. Not intended to be used separately. + * + * @extends Scroller + * @private + */ +class TabsheetScroller extends Scroller { + static get is() { + return 'vaadin-tabsheet-scroller'; + } +} + +defineCustomElement(TabsheetScroller); diff --git a/packages/tabsheet/src/vaadin-lit-tabsheet.d.ts b/packages/tabsheet/src/vaadin-lit-tabsheet.d.ts new file mode 100644 index 0000000000..cf3fdb91d5 --- /dev/null +++ b/packages/tabsheet/src/vaadin-lit-tabsheet.d.ts @@ -0,0 +1 @@ +export * from './vaadin-tabsheet.js'; diff --git a/packages/tabsheet/src/vaadin-lit-tabsheet.js b/packages/tabsheet/src/vaadin-lit-tabsheet.js new file mode 100644 index 0000000000..2c61c7aa66 --- /dev/null +++ b/packages/tabsheet/src/vaadin-lit-tabsheet.js @@ -0,0 +1,78 @@ +/** + * @license + * Copyright (c) 2022 - 2024 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import './vaadin-lit-tabsheet-scroller.js'; +import { css, html, LitElement } from 'lit'; +import { defineCustomElement } from '@vaadin/component-base/src/define.js'; +import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; +import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js'; +import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; +import { TabSheetMixin } from './vaadin-tabsheet-mixin.js'; + +/** + * LitElement based version of `` web component. + * + * ## Disclaimer + * + * This component is an experiment and not yet a part of Vaadin platform. + * There is no ETA regarding specific Vaadin version where it'll land. + * Feel free to try this code in your apps as per Apache 2.0 license. + */ +class TabSheet extends TabSheetMixin(ThemableMixin(ElementMixin(PolylitMixin(LitElement)))) { + static get is() { + return 'vaadin-tabsheet'; + } + + static get styles() { + return css` + :host { + display: flex; + flex-direction: column; + } + + :host([hidden]) { + display: none !important; + } + + [part='tabs-container'] { + position: relative; + display: flex; + align-items: center; + } + + ::slotted([slot='tabs']) { + flex: 1; + align-self: stretch; + min-width: 8em; + } + + [part='content'] { + position: relative; + flex: 1; + box-sizing: border-box; + } + `; + } + + /** @protected */ + render() { + return html` +
+ + + +
+ + +
+ +
+ `; + } +} + +defineCustomElement(TabSheet); + +export { TabSheet }; diff --git a/packages/tabsheet/src/vaadin-tabsheet-mixin.js b/packages/tabsheet/src/vaadin-tabsheet-mixin.js index 22b9d48ccd..b1baafd77c 100644 --- a/packages/tabsheet/src/vaadin-tabsheet-mixin.js +++ b/packages/tabsheet/src/vaadin-tabsheet-mixin.js @@ -123,12 +123,7 @@ export const TabSheetMixin = (superClass) => /** @override */ static get delegateProps() { - return ['selected']; - } - - /** @override */ - static get delegateAttrs() { - return ['theme']; + return ['selected', '_theme']; } /** @protected */ @@ -171,6 +166,25 @@ export const TabSheetMixin = (superClass) => }); } + /** + * Override method from `DelegateStateMixin` to set delegate `theme` + * using attribute instead of property (needed for the Lit version). + * @protected + * @override + */ + _delegateProperty(name, value) { + if (!this.stateTarget) { + return; + } + + if (name === '_theme') { + this._delegateAttribute('theme', value); + return; + } + + super._delegateProperty(name, value); + } + /** * An observer which applies the necessary roles and ARIA attributes * to associate the tab elements with the panels. diff --git a/packages/tabsheet/test/tabsheet-lit.test.js b/packages/tabsheet/test/tabsheet-lit.test.js new file mode 100644 index 0000000000..2ff9da2f76 --- /dev/null +++ b/packages/tabsheet/test/tabsheet-lit.test.js @@ -0,0 +1,3 @@ +import '@vaadin/tabs/src/vaadin-lit-tabs.js'; +import '../src/vaadin-lit-tabsheet.js'; +import './tabsheet.common.js'; diff --git a/packages/tabsheet/test/tabsheet-polymer.test.js b/packages/tabsheet/test/tabsheet-polymer.test.js new file mode 100644 index 0000000000..ebc290d7cd --- /dev/null +++ b/packages/tabsheet/test/tabsheet-polymer.test.js @@ -0,0 +1,3 @@ +import '@vaadin/tabs/src/vaadin-tabs.js'; +import '../src/vaadin-tabsheet.js'; +import './tabsheet.common.js'; diff --git a/packages/tabsheet/test/tabsheet.test.js b/packages/tabsheet/test/tabsheet.common.js similarity index 94% rename from packages/tabsheet/test/tabsheet.test.js rename to packages/tabsheet/test/tabsheet.common.js index 627bbe238a..e9925bcc1c 100644 --- a/packages/tabsheet/test/tabsheet.test.js +++ b/packages/tabsheet/test/tabsheet.common.js @@ -1,7 +1,6 @@ import { expect } from '@vaadin/chai-plugins'; import { fixtureSync, nextFrame } from '@vaadin/testing-helpers'; import sinon from 'sinon'; -import '../vaadin-tabsheet.js'; describe('tabsheet', () => { let tabsheet, tabs; @@ -54,9 +53,9 @@ describe('tabsheet', () => { expect(tabsheet.items.length).to.be.equal(3); }); - it('should update items value when the tabs items change', () => { + it('should update items value when the tabs items change', async () => { tabs.removeChild(tabsheet.items[2]); - tabs._observer.flush(); + await nextFrame(); expect(tabsheet.items.length).to.be.equal(2); }); @@ -81,8 +80,9 @@ describe('tabsheet', () => { expect(tabsheet.items[0].selected).to.be.true; }); - it('should update selected to new index when other tab is selected', () => { + it('should update selected to new index when other tab is selected', async () => { tabs.items[1].click(); + await nextFrame(); expect(tabsheet.items[1].selected).to.be.true; expect(tabsheet.selected).to.equal(1); }); @@ -92,35 +92,40 @@ describe('tabsheet', () => { await nextFrame(); tabs.selected = 1; tabs.items[1].click(); + await nextFrame(); expect(tabsheet.selected).not.to.equal(1); }); - it('should close currently selected tab when another one is selected', () => { + it('should close currently selected tab when another one is selected', async () => { tabs.items[1].click(); + await nextFrame(); expect(tabsheet.items[1].selected).to.be.true; expect(tabsheet.items[0].selected).to.be.false; }); - it('should not change selected state if tab has been removed', () => { + it('should not change selected state if tab has been removed', async () => { const tab = tabsheet.items[1]; tabs.removeChild(tab); tabs._observer.flush(); tab.selected = true; + await nextFrame(); expect(tabsheet.selected).to.equal(0); }); - it('should dispatch selected-changed event when selected changes', () => { + it('should dispatch selected-changed event when selected changes', async () => { const spy = sinon.spy(); tabsheet.addEventListener('selected-changed', spy); tabs.items[1].click(); + await nextFrame(); expect(spy.calledOnce).to.be.true; }); }); describe('syncing properties', () => { - it('should propagate selected value to tabs', () => { + it('should propagate selected value to tabs', async () => { expect(tabs.selected).to.equal(tabsheet.selected); tabsheet.selected = 1; + await nextFrame(); expect(tabs.selected).to.equal(tabsheet.selected); }); @@ -128,6 +133,7 @@ describe('tabsheet', () => { tabs.remove(); await nextFrame(); tabsheet.selected = 1; + await nextFrame(); expect(tabs.selected).not.to.equal(tabsheet.selected); }); }); @@ -139,14 +145,16 @@ describe('tabsheet', () => { expect(getPanels()[2].hidden).to.be.true; }); - it('should show another panel on tab change', () => { + it('should show another panel on tab change', async () => { tabsheet.selected = 1; + await nextFrame(); expect(getPanels()[0].hidden).to.be.true; expect(getPanels()[1].hidden).to.be.false; }); - it('should not show a panel if no matching panel found', () => { + it('should not show a panel if no matching panel found', async () => { tabsheet.selected = 3; + await nextFrame(); expect(getPanels()[0].hidden).to.be.true; expect(getPanels()[1].hidden).to.be.true; expect(getPanels()[2].hidden).to.be.true; @@ -227,8 +235,9 @@ describe('tabsheet', () => { expect(tabsheet.hasAttribute('loading')).to.be.false; }); - it('should be in loading state after opening a tab with no panel', () => { + it('should be in loading state after opening a tab with no panel', async () => { newTab.click(); + await nextFrame(); expect(tabsheet.hasAttribute('loading')).to.be.true; expect(tabsheet.getAttribute('loading')).to.equal(''); }); @@ -308,19 +317,23 @@ describe('tabsheet', () => { }); describe('theme propagation', () => { - it('should set the theme attribute to the slotted tabs', () => { + it('should set the theme attribute to the slotted tabs', async () => { tabsheet.setAttribute('theme', 'foo'); + await nextFrame(); expect(tabs.getAttribute('theme')).to.equal('foo'); }); - it('should remove the theme attribute to the slotted tabs', () => { + it('should remove the theme attribute to the slotted tabs', async () => { tabsheet.setAttribute('theme', 'foo'); + await nextFrame(); tabsheet.removeAttribute('theme'); + await nextFrame(); expect(tabs.hasAttribute('theme')).to.be.false; }); it('should set the theme attribute to newly added tabs', async () => { tabsheet.setAttribute('theme', 'foo'); + await nextFrame(); tabs.remove(); const newTabs = fixtureSync(``);