From 8acff75a9abfc83b15e064725fe04b83bdcbc366 Mon Sep 17 00:00:00 2001 From: Imran Sayed Date: Fri, 17 Nov 2023 23:45:13 -0600 Subject: [PATCH 1/7] Add JS for tab element --- src/tabs/index.html | 39 +++++++++++++++++++++++++++++++++++++++ src/tabs/index.ts | 13 +++++++++++++ src/tabs/style.scss | 0 src/tabs/tp-tab.ts | 5 +++++ src/tabs/tp-tabs-nav.ts | 5 +++++ src/tabs/tp-tabs.ts | 12 ++++++++++++ webpack.config.js | 1 + 7 files changed, 75 insertions(+) create mode 100644 src/tabs/index.html create mode 100644 src/tabs/index.ts create mode 100644 src/tabs/style.scss create mode 100644 src/tabs/tp-tab.ts create mode 100644 src/tabs/tp-tabs-nav.ts create mode 100644 src/tabs/tp-tabs.ts diff --git a/src/tabs/index.html b/src/tabs/index.html new file mode 100644 index 0000000..f4020c6 --- /dev/null +++ b/src/tabs/index.html @@ -0,0 +1,39 @@ + + + + + + + Web Component: Tabs + + + + + + +
+ + + + + +

Content

+
+ +

Content

+
+ +

Content

+
+ +

Content

+
+
+
+ + diff --git a/src/tabs/index.ts b/src/tabs/index.ts new file mode 100644 index 0000000..79c33b4 --- /dev/null +++ b/src/tabs/index.ts @@ -0,0 +1,13 @@ +/** + * Components. + */ +import { TPTabElement } from './tp-tab'; +import { TPTabs } from './tp-tabs'; +import { TPTabsNavElement } from './tp-tabs-nav'; + +/** + * Register Components. + */ +customElements.define( 'tp-tabs', TPTabs ); +customElements.define( 'tp-tab', TPTabElement ); +customElements.define( 'tp-tabs-nav', TPTabsNavElement ); diff --git a/src/tabs/style.scss b/src/tabs/style.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/tabs/tp-tab.ts b/src/tabs/tp-tab.ts new file mode 100644 index 0000000..7ad2e88 --- /dev/null +++ b/src/tabs/tp-tab.ts @@ -0,0 +1,5 @@ +/** + * TP Tab Element. + */ +export class TPTabElement extends HTMLElement { +} diff --git a/src/tabs/tp-tabs-nav.ts b/src/tabs/tp-tabs-nav.ts new file mode 100644 index 0000000..dfb1b7b --- /dev/null +++ b/src/tabs/tp-tabs-nav.ts @@ -0,0 +1,5 @@ +/** + * TP Tabs Nav Element. + */ +export class TPTabsNavElement extends HTMLElement { +} diff --git a/src/tabs/tp-tabs.ts b/src/tabs/tp-tabs.ts new file mode 100644 index 0000000..5da34dd --- /dev/null +++ b/src/tabs/tp-tabs.ts @@ -0,0 +1,12 @@ +/** + * TP Slider. + */ +export class TPTabs extends HTMLElement { + /** + * Constructor. + */ + constructor() { + super(); + console.log( 'd' ); + } +} diff --git a/webpack.config.js b/webpack.config.js index 53e0545..79f8d0a 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -78,6 +78,7 @@ module.exports = ( env ) => { entry: { modal: './src/modal/index.ts', slider: './src/slider/index.ts', + tabs: './src/tabs/index.ts', }, module: { rules: [ From 3f9d55c323fb5f8a539578e54f273903daae5c0f Mon Sep 17 00:00:00 2001 From: Imran Sayed Date: Sun, 19 Nov 2023 22:59:56 -0600 Subject: [PATCH 2/7] Add JS for tabs --- src/tabs/index.html | 24 ++++-- src/tabs/tp-tab-nav.ts | 5 ++ src/tabs/tp-tabs.ts | 168 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 src/tabs/tp-tab-nav.ts diff --git a/src/tabs/index.html b/src/tabs/index.html index f4020c6..b7b55d7 100644 --- a/src/tabs/index.html +++ b/src/tabs/index.html @@ -15,23 +15,31 @@ -

Content

+

Tab 1: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-

Content

+

Tab 2: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-

Content

+

Tab 3: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-

Content

+

Tab 4: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

diff --git a/src/tabs/tp-tab-nav.ts b/src/tabs/tp-tab-nav.ts new file mode 100644 index 0000000..4fbd6d9 --- /dev/null +++ b/src/tabs/tp-tab-nav.ts @@ -0,0 +1,5 @@ +/** + * TP Tab Nav Element. + */ +export class TPTabNavElement extends HTMLElement { +} diff --git a/src/tabs/tp-tabs.ts b/src/tabs/tp-tabs.ts index 5da34dd..32af394 100644 --- a/src/tabs/tp-tabs.ts +++ b/src/tabs/tp-tabs.ts @@ -1,12 +1,178 @@ +/** + * Internal dependencies. + */ +import { TPTabNavElement } from './tp-tab-nav'; +import { TPTabElement } from './tp-tab'; + /** * TP Slider. */ export class TPTabs extends HTMLElement { + /** + * Properties. + * + * @private + */ + private tabLinks: NodeListOf; + private tabNavs: NodeListOf; + private activeTabId: string; + private tabs: NodeListOf; + /** * Constructor. */ constructor() { super(); - console.log( 'd' ); + + // Elements. + this.tabNavs = this.querySelectorAll( 'tp-tab-nav' ); + this.tabLinks = this.querySelectorAll( 'tp-tab-nav a' ); + this.tabs = this.querySelectorAll( 'tp-tab' ); + this.activeTabId = ''; + + this.initializeTabs(); + } + + /** + * Initialize Tabs. + * + * @return {void} Null. + */ + initializeTabs(): void { + // Check if tab links exists. + if ( ! this.tabLinks ) { + // No, bail early. + return; + } + + // Handle Events for Tab Links. + this.tabLinks.forEach( ( tabLink: HTMLAnchorElement ) => { + // Handle the click event. + tabLink.addEventListener( 'click', ( event ) => { + // Trigger the event. + this.handleTabLinkClick( event ); + } ); + + // Handle the touchend event. + tabLink.addEventListener( 'touchend', ( event ) => { + // Trigger the event. + if ( event.cancelable ) { + this.handleTabLinkClick( event ); + } + } ); + } ); + } + + /** + * Event: Link clicked. + * + * @param {Event} event Event. + * + * @return {void} Null. + */ + handleTabLinkClick( event: Event ): void { + /** + * Prevent Default is added remove browser horizontal scroll on mobile + * and allow our custom animated scroll. + */ + event.preventDefault(); + const clickedTabLink = event.target as HTMLAnchorElement; + + // Checked for tab link clicked. + if ( ! clickedTabLink ) { + // No, bail early. + return; + } + + // Events. + this.triggerTabSelection( clickedTabLink ); + + // Add hash to the url, which is not added by default because of adding `event.preventDefault()'. @TODO handle URL's without trailing slash. + window.location.hash = `${ this.activeTabId }`; + } + + /** + * Trigger Tab Selection. + * + * @param {HTMLElement} clickedTabLink + * + * @return {void} Null. + */ + triggerTabSelection( clickedTabLink: HTMLAnchorElement ): void { + // Check if clicked tab link exist. + if ( ! clickedTabLink ) { + // No, bail early. + return; + } + + // Get the values. + this.activeTabId = clickedTabLink.getAttribute( 'href' ) || ''; + console.log( 'this.activeTabId', this.activeTabId ); + + // Events. + this.toggleTabLinkAttributes( clickedTabLink ); + this.toggleTabContentVisibility(); + } + + /** + * Toggle Tab Link Attributes. + * + * @param {Object} clickedTabLink + * @return {void} Null. + */ + toggleTabLinkAttributes( clickedTabLink: HTMLAnchorElement ): void { + // Check if the tab navs exist or clicked. + if ( ! this.tabNavs || ! clickedTabLink ) { + // No, bail early. + return; + } + console.log( 'this.tabNavs', this.tabNavs ); + + // 1. First remove active attribute from all tab links. + this.tabNavs.forEach( ( tabNav: TPTabNavElement ) => { + // Remove the active attribute. + tabNav.removeAttribute( 'active' ); + } ); + + // 2. Then set the 'active' attribute to the clicked tab link's parent. + if ( clickedTabLink.parentElement ) { + console.log( 'clickedTabLink', clickedTabLink.parentElement ); + clickedTabLink.parentElement.setAttribute( 'active', 'true' ); + } + } + + /** + * Toggle Tab Content Visibility. + * + * Toggle the 'active' attributes, so that only + * the clicked tab gets visible. + * + * @return {void} Null. + */ + toggleTabContentVisibility(): void { + // Check if the tab content is exist. + if ( ! this.tabs ) { + // No, bail early. + return; + } + + // Set tab ID. + let tabId:string|null = ''; + + // Handle the tab content items. + this.tabs.forEach( ( tab: TPTabElement ) => { + // Get the tab ID. + tabId = tab.getAttribute( 'id' ); + + /** + * If the tabId of the content is same as the clicked tab-link id, + * remove 'active' attribute to make the respective content visible. + */ + if ( this.activeTabId === tabId ) { + tab.setAttribute( 'active', 'true' ); + } else { + tab.removeAttribute( 'active' ); + } + } ); } } From 9caeafe44fc9285729242232369633d52a4ba6c5 Mon Sep 17 00:00:00 2001 From: Imran Sayed Date: Mon, 20 Nov 2023 00:06:09 -0600 Subject: [PATCH 3/7] Update tab component --- src/tabs/README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++ src/tabs/index.html | 14 +++++++++ src/tabs/index.ts | 5 +++ src/tabs/style.scss | 7 +++++ src/tabs/tp-tabs.ts | 74 ++++++++++++++++++++++++++++++++++---------- 5 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/tabs/README.md diff --git a/src/tabs/README.md b/src/tabs/README.md new file mode 100644 index 0000000..2e29d45 --- /dev/null +++ b/src/tabs/README.md @@ -0,0 +1,75 @@ +# Tabs + + + + + + +
+

Built by the super talented team at Travelopia.

+
+ +
+ +## Sample Usage + +Example: + +```js +// Import the component as needed: +import '@travelopia/web-components/dist/tabs'; +import '@travelopia/web-components/dist/tabs/style.css'; + +// TypeScript usage: +import { TPTabs } from '@travelopia/web-components'; + +... + +const tabs: TPTabs = document.querySelector( 'tp-tabs' ); +tabs.triggerTabSelection( '#overview' ); +``` + +```html + + + + + +

Tab 1: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

+
+ +

Tab 2: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

+
+ +

Tab 3: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

+
+ +

Tab 4: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

+
+
+``` + +## Events + +| Event | Notes | +|----------------|-----------------------------------------------------------------------------------------------------| +| tp-tab-clicked | When a tab is clicked. You can access the tab id using activeTabId.activeTabId e.g. value #overview | + +## Methods + +### `triggerTabSelection` + +Move to the tab with the given id. diff --git a/src/tabs/index.html b/src/tabs/index.html index b7b55d7..29f9ce6 100644 --- a/src/tabs/index.html +++ b/src/tabs/index.html @@ -9,6 +9,20 @@ + +
diff --git a/src/tabs/index.ts b/src/tabs/index.ts index 79c33b4..158d0f6 100644 --- a/src/tabs/index.ts +++ b/src/tabs/index.ts @@ -1,3 +1,8 @@ +/** + * Styles. + */ +import './style.scss'; + /** * Components. */ diff --git a/src/tabs/style.scss b/src/tabs/style.scss index e69de29..2af7d1a 100644 --- a/src/tabs/style.scss +++ b/src/tabs/style.scss @@ -0,0 +1,7 @@ +tp-tab { + display: none; + + &[open="yes"] { + display: block; + } +} diff --git a/src/tabs/tp-tabs.ts b/src/tabs/tp-tabs.ts index 32af394..c28988e 100644 --- a/src/tabs/tp-tabs.ts +++ b/src/tabs/tp-tabs.ts @@ -30,7 +30,9 @@ export class TPTabs extends HTMLElement { this.tabs = this.querySelectorAll( 'tp-tab' ); this.activeTabId = ''; + // Initialize tabs. this.initializeTabs(); + this.autoScrollToUrlAnchoredId(); } /** @@ -84,8 +86,11 @@ export class TPTabs extends HTMLElement { return; } + // Get the active tab id. + this.activeTabId = clickedTabLink.getAttribute( 'href' ) || ''; + // Events. - this.triggerTabSelection( clickedTabLink ); + this.triggerTabSelection( this.activeTabId ); // Add hash to the url, which is not added by default because of adding `event.preventDefault()'. @TODO handle URL's without trailing slash. window.location.hash = `${ this.activeTabId }`; @@ -94,49 +99,51 @@ export class TPTabs extends HTMLElement { /** * Trigger Tab Selection. * - * @param {HTMLElement} clickedTabLink + * @param {string} activeTabId * * @return {void} Null. */ - triggerTabSelection( clickedTabLink: HTMLAnchorElement ): void { + triggerTabSelection( activeTabId: string ): void { // Check if clicked tab link exist. - if ( ! clickedTabLink ) { + if ( ! activeTabId ) { // No, bail early. return; } - // Get the values. - this.activeTabId = clickedTabLink.getAttribute( 'href' ) || ''; - console.log( 'this.activeTabId', this.activeTabId ); - // Events. - this.toggleTabLinkAttributes( clickedTabLink ); + this.toggleTabLinkAttributes( activeTabId ); this.toggleTabContentVisibility(); + this.dispatchEvent( new CustomEvent( 'tp-tab-clicked', { + detail: { + activeTabId: this.activeTabId, + }, + } ) ); } /** * Toggle Tab Link Attributes. * - * @param {Object} clickedTabLink + * @param {string} activeTabId * @return {void} Null. */ - toggleTabLinkAttributes( clickedTabLink: HTMLAnchorElement ): void { + toggleTabLinkAttributes( activeTabId: string ): void { + // Get clicked tab link. + const clickedTabLink = this.querySelector( `[href="${ activeTabId }"]` ); + // Check if the tab navs exist or clicked. if ( ! this.tabNavs || ! clickedTabLink ) { // No, bail early. return; } - console.log( 'this.tabNavs', this.tabNavs ); - // 1. First remove active attribute from all tab links. + // 1. First remove active attribute from all nav elements. this.tabNavs.forEach( ( tabNav: TPTabNavElement ) => { // Remove the active attribute. tabNav.removeAttribute( 'active' ); } ); - // 2. Then set the 'active' attribute to the clicked tab link's parent. + // 2. Then set the 'active' attribute to the clicked nav element ( tab link's parent ). if ( clickedTabLink.parentElement ) { - console.log( 'clickedTabLink', clickedTabLink.parentElement ); clickedTabLink.parentElement.setAttribute( 'active', 'true' ); } } @@ -169,10 +176,43 @@ export class TPTabs extends HTMLElement { * remove 'active' attribute to make the respective content visible. */ if ( this.activeTabId === tabId ) { - tab.setAttribute( 'active', 'true' ); + tab.setAttribute( 'open', 'yes' ); } else { - tab.removeAttribute( 'active' ); + tab.removeAttribute( 'open' ); } } ); } + + /** + * Auto Scroll to Anchored URL. + * + * On initial render - let's say user click on an anchored url, shared by someone, + * if the url has the id of one of the tabs, auto scroll to that tab link. + */ + autoScrollToUrlAnchoredId(): void { + // Get currentTabId ( e.g. #itinerary ) from the current URL. + const currentTabId: string | string[] = window?.location?.hash ?? ''; + + // Check if current tab ID exist. + if ( ! currentTabId ) { + // No, bail early. + return; + } + + // Elements. + const tabs = this.querySelectorAll( 'tp-tab' ); + const tabIds:Array = []; + + // Tab links index. + tabs.forEach( ( tab, index ) => { + // Set tab ids. + tabIds[ index ] = tab.getAttribute( 'id' ); + } ); + + // If the id in the request url is one of the ids from the tab links, scroll to that tab link. + if ( tabIds && tabIds.includes( currentTabId ) ) { + this.activeTabId = currentTabId; + this.triggerTabSelection( currentTabId ); + } + } } From a2b30e30c67cf124dc98716e31a052e524df60c5 Mon Sep 17 00:00:00 2001 From: Imran Sayed Date: Mon, 20 Nov 2023 00:24:52 -0600 Subject: [PATCH 4/7] Update doc comment --- src/tabs/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tabs/index.ts b/src/tabs/index.ts index 158d0f6..ce5b1d7 100644 --- a/src/tabs/index.ts +++ b/src/tabs/index.ts @@ -8,6 +8,7 @@ import './style.scss'; */ import { TPTabElement } from './tp-tab'; import { TPTabs } from './tp-tabs'; +import { TPTabNavElement } from './tp-tab-nav'; import { TPTabsNavElement } from './tp-tabs-nav'; /** @@ -15,4 +16,5 @@ import { TPTabsNavElement } from './tp-tabs-nav'; */ customElements.define( 'tp-tabs', TPTabs ); customElements.define( 'tp-tab', TPTabElement ); +customElements.define( 'tp-tab-nav', TPTabNavElement ); customElements.define( 'tp-tabs-nav', TPTabsNavElement ); From a8da6bbf8e715cf390f02e3c122a3442e2e8bfb4 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 21 Nov 2023 10:43:02 +1100 Subject: [PATCH 5/7] update tabs components --- src/tabs/README.md | 16 +-- src/tabs/index.html | 50 ++++---- src/tabs/index.ts | 13 +- src/tabs/style.scss | 2 +- src/tabs/tp-tab-nav.ts | 5 - src/tabs/tp-tab.ts | 5 - src/tabs/tp-tabs-nav-item.ts | 31 +++++ src/tabs/tp-tabs-tab.ts | 5 + src/tabs/tp-tabs.ts | 233 +++++++---------------------------- 9 files changed, 119 insertions(+), 241 deletions(-) delete mode 100644 src/tabs/tp-tab-nav.ts delete mode 100644 src/tabs/tp-tab.ts create mode 100644 src/tabs/tp-tabs-nav-item.ts create mode 100644 src/tabs/tp-tabs-tab.ts diff --git a/src/tabs/README.md b/src/tabs/README.md index 2e29d45..a96fc9b 100644 --- a/src/tabs/README.md +++ b/src/tabs/README.md @@ -33,18 +33,18 @@ tabs.triggerTabSelection( '#overview' ); diff --git a/src/tabs/index.html b/src/tabs/index.html index 29f9ce6..35c667d 100644 --- a/src/tabs/index.html +++ b/src/tabs/index.html @@ -10,7 +10,7 @@ @@ -26,35 +26,33 @@
- - - + + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + Tab 4 + - +

Tab 1: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
- + +

Tab 2: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
- + +

Tab 3: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
- + +

Tab 4: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
+
diff --git a/src/tabs/index.ts b/src/tabs/index.ts index ce5b1d7..7787fbd 100644 --- a/src/tabs/index.ts +++ b/src/tabs/index.ts @@ -6,15 +6,16 @@ import './style.scss'; /** * Components. */ -import { TPTabElement } from './tp-tab'; -import { TPTabs } from './tp-tabs'; -import { TPTabNavElement } from './tp-tab-nav'; +import { TPTabsNavItemElement } from './tp-tabs-nav-item'; import { TPTabsNavElement } from './tp-tabs-nav'; +import { TPTabsTabElement } from './tp-tabs-tab'; +import { TPTabsElement } from './tp-tabs'; /** * Register Components. */ -customElements.define( 'tp-tabs', TPTabs ); -customElements.define( 'tp-tab', TPTabElement ); -customElements.define( 'tp-tab-nav', TPTabNavElement ); + +customElements.define( 'tp-tabs-nav-item', TPTabsNavItemElement ); customElements.define( 'tp-tabs-nav', TPTabsNavElement ); +customElements.define( 'tp-tabs-tab', TPTabsTabElement ); +customElements.define( 'tp-tabs', TPTabsElement ); diff --git a/src/tabs/style.scss b/src/tabs/style.scss index 2af7d1a..4dd66fb 100644 --- a/src/tabs/style.scss +++ b/src/tabs/style.scss @@ -1,4 +1,4 @@ -tp-tab { +tp-tabs-tab { display: none; &[open="yes"] { diff --git a/src/tabs/tp-tab-nav.ts b/src/tabs/tp-tab-nav.ts deleted file mode 100644 index 4fbd6d9..0000000 --- a/src/tabs/tp-tab-nav.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * TP Tab Nav Element. - */ -export class TPTabNavElement extends HTMLElement { -} diff --git a/src/tabs/tp-tab.ts b/src/tabs/tp-tab.ts deleted file mode 100644 index 7ad2e88..0000000 --- a/src/tabs/tp-tab.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * TP Tab Element. - */ -export class TPTabElement extends HTMLElement { -} diff --git a/src/tabs/tp-tabs-nav-item.ts b/src/tabs/tp-tabs-nav-item.ts new file mode 100644 index 0000000..e8948c9 --- /dev/null +++ b/src/tabs/tp-tabs-nav-item.ts @@ -0,0 +1,31 @@ +/** + * Internal dependencies. + */ +import { TPTabsElement } from './tp-tabs'; + +/** + * TP Tabs Nav Item Element. + */ +export class TPTabsNavItemElement extends HTMLElement { + connectedCallback(): void { + const link: HTMLAnchorElement | null = this.querySelector( 'a' ); + link?.addEventListener( 'click', this.setTab.bind( this ) ); + } + + setTab(): void { + const tabs: TPTabsElement | null = this.closest( 'tp-tabs' ); + const link: HTMLAnchorElement | null = this.querySelector( 'a' ); + const anchor: string = link?.getAttribute( 'href' ) ?? ''; + + if ( ! tabs || ! link || '' === anchor ) { + return; + } + + tabs.setAttribute( 'current-tab', anchor.replace( '#', '' ) ); + } + + isCurrentTab( currentTab: string = '' ): boolean { + const link: HTMLAnchorElement | null = this.querySelector( 'a' ); + return `#${ currentTab }` === link?.getAttribute( 'href' ); + } +} diff --git a/src/tabs/tp-tabs-tab.ts b/src/tabs/tp-tabs-tab.ts new file mode 100644 index 0000000..d065ebc --- /dev/null +++ b/src/tabs/tp-tabs-tab.ts @@ -0,0 +1,5 @@ +/** + * TP Tabs Tab Element. + */ +export class TPTabsTabElement extends HTMLElement { +} diff --git a/src/tabs/tp-tabs.ts b/src/tabs/tp-tabs.ts index c28988e..81f9ed4 100644 --- a/src/tabs/tp-tabs.ts +++ b/src/tabs/tp-tabs.ts @@ -1,218 +1,71 @@ /** * Internal dependencies. */ -import { TPTabNavElement } from './tp-tab-nav'; -import { TPTabElement } from './tp-tab'; + +import { TPTabsNavItemElement } from './tp-tabs-nav-item'; +import { TPTabsTabElement } from './tp-tabs-tab'; /** - * TP Slider. + * TP Tabs. */ -export class TPTabs extends HTMLElement { - /** - * Properties. - * - * @private - */ - private tabLinks: NodeListOf; - private tabNavs: NodeListOf; - private activeTabId: string; - private tabs: NodeListOf; - - /** - * Constructor. - */ - constructor() { - super(); - - // Elements. - this.tabNavs = this.querySelectorAll( 'tp-tab-nav' ); - this.tabLinks = this.querySelectorAll( 'tp-tab-nav a' ); - this.tabs = this.querySelectorAll( 'tp-tab' ); - this.activeTabId = ''; - - // Initialize tabs. - this.initializeTabs(); - this.autoScrollToUrlAnchoredId(); - } - - /** - * Initialize Tabs. - * - * @return {void} Null. - */ - initializeTabs(): void { - // Check if tab links exists. - if ( ! this.tabLinks ) { - // No, bail early. - return; - } - - // Handle Events for Tab Links. - this.tabLinks.forEach( ( tabLink: HTMLAnchorElement ) => { - // Handle the click event. - tabLink.addEventListener( 'click', ( event ) => { - // Trigger the event. - this.handleTabLinkClick( event ); - } ); - - // Handle the touchend event. - tabLink.addEventListener( 'touchend', ( event ) => { - // Trigger the event. - if ( event.cancelable ) { - this.handleTabLinkClick( event ); - } - } ); - } ); - } - - /** - * Event: Link clicked. - * - * @param {Event} event Event. - * - * @return {void} Null. - */ - handleTabLinkClick( event: Event ): void { - /** - * Prevent Default is added remove browser horizontal scroll on mobile - * and allow our custom animated scroll. - */ - event.preventDefault(); - const clickedTabLink = event.target as HTMLAnchorElement; - - // Checked for tab link clicked. - if ( ! clickedTabLink ) { - // No, bail early. - return; - } - - // Get the active tab id. - this.activeTabId = clickedTabLink.getAttribute( 'href' ) || ''; - - // Events. - this.triggerTabSelection( this.activeTabId ); - - // Add hash to the url, which is not added by default because of adding `event.preventDefault()'. @TODO handle URL's without trailing slash. - window.location.hash = `${ this.activeTabId }`; - } - +export class TPTabsElement extends HTMLElement { /** - * Trigger Tab Selection. - * - * @param {string} activeTabId + * Get observed attributes. * - * @return {void} Null. + * @return {Array} List of observed attributes. */ - triggerTabSelection( activeTabId: string ): void { - // Check if clicked tab link exist. - if ( ! activeTabId ) { - // No, bail early. - return; - } - - // Events. - this.toggleTabLinkAttributes( activeTabId ); - this.toggleTabContentVisibility(); - this.dispatchEvent( new CustomEvent( 'tp-tab-clicked', { - detail: { - activeTabId: this.activeTabId, - }, - } ) ); + static get observedAttributes(): string[] { + return [ 'current-tab', 'update-url', 'overflow' ]; } /** - * Toggle Tab Link Attributes. + * Attribute changed callback. * - * @param {string} activeTabId - * @return {void} Null. + * @param {string} name Attribute name. + * @param {string} oldValue Old value. + * @param {string} newValue New value. */ - toggleTabLinkAttributes( activeTabId: string ): void { - // Get clicked tab link. - const clickedTabLink = this.querySelector( `[href="${ activeTabId }"]` ); - - // Check if the tab navs exist or clicked. - if ( ! this.tabNavs || ! clickedTabLink ) { - // No, bail early. + attributeChangedCallback( name: string = '', oldValue: string = '', newValue: string = '' ): void { + if ( oldValue === newValue ) { return; } - // 1. First remove active attribute from all nav elements. - this.tabNavs.forEach( ( tabNav: TPTabNavElement ) => { - // Remove the active attribute. - tabNav.removeAttribute( 'active' ); - } ); + this.update(); - // 2. Then set the 'active' attribute to the clicked nav element ( tab link's parent ). - if ( clickedTabLink.parentElement ) { - clickedTabLink.parentElement.setAttribute( 'active', 'true' ); - } - } - - /** - * Toggle Tab Content Visibility. - * - * Toggle the 'active' attributes, so that only - * the clicked tab gets visible. - * - * @return {void} Null. - */ - toggleTabContentVisibility(): void { - // Check if the tab content is exist. - if ( ! this.tabs ) { - // No, bail early. - return; + if ( 'current-tab' === name ) { + this.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) ); } - - // Set tab ID. - let tabId:string|null = ''; - - // Handle the tab content items. - this.tabs.forEach( ( tab: TPTabElement ) => { - // Get the tab ID. - tabId = tab.getAttribute( 'id' ); - - /** - * If the tabId of the content is same as the clicked tab-link id, - * remove 'active' attribute to make the respective content visible. - */ - if ( this.activeTabId === tabId ) { - tab.setAttribute( 'open', 'yes' ); - } else { - tab.removeAttribute( 'open' ); - } - } ); } /** - * Auto Scroll to Anchored URL. - * - * On initial render - let's say user click on an anchored url, shared by someone, - * if the url has the id of one of the tabs, auto scroll to that tab link. + * Update this component. */ - autoScrollToUrlAnchoredId(): void { - // Get currentTabId ( e.g. #itinerary ) from the current URL. - const currentTabId: string | string[] = window?.location?.hash ?? ''; - - // Check if current tab ID exist. - if ( ! currentTabId ) { - // No, bail early. - return; + update(): void { + // Get current tab. + const currentTab: string = this.getAttribute( 'current-tab' ) ?? ''; + + // Update nav items. + const navItems: NodeListOf = this.querySelectorAll( 'tp-tabs-nav-item' ); + if ( navItems ) { + navItems.forEach( ( navItem: TPTabsNavItemElement ): void => { + if ( navItem.isCurrentTab( currentTab ) ) { + navItem.setAttribute( 'active', 'yes' ); + } else { + navItem.removeAttribute( 'active' ); + } + } ); } - // Elements. - const tabs = this.querySelectorAll( 'tp-tab' ); - const tabIds:Array = []; - - // Tab links index. - tabs.forEach( ( tab, index ) => { - // Set tab ids. - tabIds[ index ] = tab.getAttribute( 'id' ); - } ); - - // If the id in the request url is one of the ids from the tab links, scroll to that tab link. - if ( tabIds && tabIds.includes( currentTabId ) ) { - this.activeTabId = currentTabId; - this.triggerTabSelection( currentTabId ); + // Update tabs. + const tabs: NodeListOf = this.querySelectorAll( 'tp-tabs-tab' ); + if ( tabs ) { + tabs.forEach( ( tab: TPTabsTabElement ): void => { + if ( currentTab === tab.getAttribute( 'id' ) ) { + tab.setAttribute( 'open', 'yes' ); + } else { + tab.removeAttribute( 'open' ); + } + } ); } } } From 3632eed28f1a30a138e3c20911e66365dbf727e1 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 21 Nov 2023 11:04:15 +1100 Subject: [PATCH 6/7] update tab url --- src/tabs/README.md | 61 +++++++++++++++++++----------------- src/tabs/index.html | 2 +- src/tabs/tp-tabs-nav-item.ts | 25 +++++++++++++-- src/tabs/tp-tabs.ts | 36 +++++++++++++++++++++ 4 files changed, 93 insertions(+), 31 deletions(-) diff --git a/src/tabs/README.md b/src/tabs/README.md index a96fc9b..b3873fa 100644 --- a/src/tabs/README.md +++ b/src/tabs/README.md @@ -26,47 +26,52 @@ import { TPTabs } from '@travelopia/web-components'; ... const tabs: TPTabs = document.querySelector( 'tp-tabs' ); -tabs.triggerTabSelection( '#overview' ); +tabs.setCurrentTab( 'overview' ); ``` ```html - - - + <-- ID without the hash + + + Tab 1 <-- This component requires a link + + + Tab 2 + + + Tab 3 + + + Tab 4 + - +

Tab 1: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
- + +

Tab 2: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
- + +

Tab 3: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
- + +

Tab 4: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

-
+
``` +## Attributes + +| Attribute | Required | Values | Notes | +|-------------|----------|-------------------------|-----------------------------------------------------| +| current-tab | Yes | | This attribute controls which tab is currently open | +| update-url | No | `yes` | Whether or not to update the has in the URL | + ## Events -| Event | Notes | -|----------------|-----------------------------------------------------------------------------------------------------| -| tp-tab-clicked | When a tab is clicked. You can access the tab id using activeTabId.activeTabId e.g. value #overview | +| Event | Notes | +|--------|------------------------| +| change | When a tab has changed | ## Methods diff --git a/src/tabs/index.html b/src/tabs/index.html index 35c667d..14230b0 100644 --- a/src/tabs/index.html +++ b/src/tabs/index.html @@ -26,7 +26,7 @@
- + Tab 1 diff --git a/src/tabs/tp-tabs-nav-item.ts b/src/tabs/tp-tabs-nav-item.ts index e8948c9..514c9fe 100644 --- a/src/tabs/tp-tabs-nav-item.ts +++ b/src/tabs/tp-tabs-nav-item.ts @@ -7,12 +7,22 @@ import { TPTabsElement } from './tp-tabs'; * TP Tabs Nav Item Element. */ export class TPTabsNavItemElement extends HTMLElement { + /** + * Connected callback. + */ connectedCallback(): void { const link: HTMLAnchorElement | null = this.querySelector( 'a' ); - link?.addEventListener( 'click', this.setTab.bind( this ) ); + link?.addEventListener( 'click', this.handleLinkClick.bind( this ) ); } - setTab(): void { + /** + * Handle link click. + * + * @param {Event} e Click event. + * + * @protected + */ + protected handleLinkClick( e: Event ): void { const tabs: TPTabsElement | null = this.closest( 'tp-tabs' ); const link: HTMLAnchorElement | null = this.querySelector( 'a' ); const anchor: string = link?.getAttribute( 'href' ) ?? ''; @@ -21,9 +31,20 @@ export class TPTabsNavItemElement extends HTMLElement { return; } + if ( 'yes' !== tabs.getAttribute( 'update-url' ) ) { + e.preventDefault(); + } + tabs.setAttribute( 'current-tab', anchor.replace( '#', '' ) ); } + /** + * Check if this component contains the link to the current tab. + * + * @param {string} currentTab Current tab ID. + * + * @return {boolean} Whether it is the current tab or not. + */ isCurrentTab( currentTab: string = '' ): boolean { const link: HTMLAnchorElement | null = this.querySelector( 'a' ); return `#${ currentTab }` === link?.getAttribute( 'href' ); diff --git a/src/tabs/tp-tabs.ts b/src/tabs/tp-tabs.ts index 81f9ed4..8e5e1ef 100644 --- a/src/tabs/tp-tabs.ts +++ b/src/tabs/tp-tabs.ts @@ -9,6 +9,14 @@ import { TPTabsTabElement } from './tp-tabs-tab'; * TP Tabs. */ export class TPTabsElement extends HTMLElement { + /** + * Connected callback. + */ + connectedCallback(): void { + this.updateTabFromUrlHash(); + window.addEventListener( 'hashchange', this.updateTabFromUrlHash.bind( this ) ); + } + /** * Get observed attributes. * @@ -44,6 +52,11 @@ export class TPTabsElement extends HTMLElement { // Get current tab. const currentTab: string = this.getAttribute( 'current-tab' ) ?? ''; + // Check if current tab exists. + if ( ! this.querySelector( `tp-tabs-tab[id="${ currentTab }"]` ) ) { + return; + } + // Update nav items. const navItems: NodeListOf = this.querySelectorAll( 'tp-tabs-nav-item' ); if ( navItems ) { @@ -68,4 +81,27 @@ export class TPTabsElement extends HTMLElement { } ); } } + + /** + * Update tab from URL hash. + */ + updateTabFromUrlHash(): void { + if ( 'yes' !== this.getAttribute( 'update-url' ) ) { + return; + } + + const urlHash: string = window.location.hash; + if ( '' !== urlHash ) { + this.setAttribute( 'current-tab', urlHash.replace( '#', '' ) ); + } + } + + /** + * Set current tab. + * + * @param {string} tabId Tab ID. + */ + setCurrentTab( tabId: string = '' ): void { + this.setAttribute( 'current-tab', tabId ); + } } From bda9bf56bd396b0201a7bd065884b01eb069fa81 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 21 Nov 2023 11:05:21 +1100 Subject: [PATCH 7/7] bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b747a72..62bdee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@travelopia/web-components", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@travelopia/web-components", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@wordpress/eslint-plugin": "^17.1.0", diff --git a/package.json b/package.json index ccd396f..2b5ac4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@travelopia/web-components", - "version": "0.2.0", + "version": "0.3.0", "description": "Accessible web components for the modern web", "files": [ "dist"