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" diff --git a/src/tabs/README.md b/src/tabs/README.md new file mode 100644 index 0000000..b3873fa --- /dev/null +++ b/src/tabs/README.md @@ -0,0 +1,80 @@ +# 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.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 | +|--------|------------------------| +| change | When a tab has changed | + +## Methods + +### `triggerTabSelection` + +Move to the tab with the given id. diff --git a/src/tabs/index.html b/src/tabs/index.html new file mode 100644 index 0000000..14230b0 --- /dev/null +++ b/src/tabs/index.html @@ -0,0 +1,59 @@ + + + + + + + Web Component: Tabs + + + + + + + + +
+ + + + 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 new file mode 100644 index 0000000..7787fbd --- /dev/null +++ b/src/tabs/index.ts @@ -0,0 +1,21 @@ +/** + * Styles. + */ +import './style.scss'; + +/** + * Components. + */ +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-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 new file mode 100644 index 0000000..4dd66fb --- /dev/null +++ b/src/tabs/style.scss @@ -0,0 +1,7 @@ +tp-tabs-tab { + display: none; + + &[open="yes"] { + display: block; + } +} diff --git a/src/tabs/tp-tabs-nav-item.ts b/src/tabs/tp-tabs-nav-item.ts new file mode 100644 index 0000000..514c9fe --- /dev/null +++ b/src/tabs/tp-tabs-nav-item.ts @@ -0,0 +1,52 @@ +/** + * Internal dependencies. + */ +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.handleLinkClick.bind( this ) ); + } + + /** + * 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' ) ?? ''; + + if ( ! tabs || ! link || '' === anchor ) { + 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-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-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 new file mode 100644 index 0000000..8e5e1ef --- /dev/null +++ b/src/tabs/tp-tabs.ts @@ -0,0 +1,107 @@ +/** + * Internal dependencies. + */ + +import { TPTabsNavItemElement } from './tp-tabs-nav-item'; +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. + * + * @return {Array} List of observed attributes. + */ + static get observedAttributes(): string[] { + return [ 'current-tab', 'update-url', 'overflow' ]; + } + + /** + * Attribute changed callback. + * + * @param {string} name Attribute name. + * @param {string} oldValue Old value. + * @param {string} newValue New value. + */ + attributeChangedCallback( name: string = '', oldValue: string = '', newValue: string = '' ): void { + if ( oldValue === newValue ) { + return; + } + + this.update(); + + if ( 'current-tab' === name ) { + this.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) ); + } + } + + /** + * Update this component. + */ + update(): void { + // 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 ) { + navItems.forEach( ( navItem: TPTabsNavItemElement ): void => { + if ( navItem.isCurrentTab( currentTab ) ) { + navItem.setAttribute( 'active', 'yes' ); + } else { + navItem.removeAttribute( 'active' ); + } + } ); + } + + // 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' ); + } + } ); + } + } + + /** + * 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 ); + } +} diff --git a/webpack.config.js b/webpack.config.js index e738954..3c61ea5 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', form: './src/form/index.ts', }, module: {