diff --git a/package-lock.json b/package-lock.json index e6b72fa..93832d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3620,9 +3620,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001650", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001650.tgz", - "integrity": "sha512-fgEc7hP/LB7iicdXHUI9VsBsMZmUmlVJeQP2qqQW+3lkqVhbmjEU8zp+h5stWeilX+G7uXuIUIIlWlDw9jdt8g==", + "version": "1.0.30001702", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz", + "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==", "dev": true, "funding": [ { diff --git a/src/tooltip/README.md b/src/tooltip/README.md new file mode 100644 index 0000000..13630b1 --- /dev/null +++ b/src/tooltip/README.md @@ -0,0 +1,64 @@ +# Tooltip + + + + + + +
+

Built by the super talented team at Travelopia.

+
+ +
+ +## Sample Usage + +This is a highly customizable tooltip component. + +Example: + +```js +// Import the component as needed: +import '@travelopia/web-components/dist/tooltip'; +import '@travelopia/web-components/dist/tooltip/style.css'; + +// No JavaScript is required to initialise! +``` + +```html + <-- Define and style the tooltip, and give it an ID + + <-- This is where the content of the tooltip would go + + <-- If you want an arrow + + +

+ Here is some informative content about + + <-- Make any element a tooltip trigger by wrapping this component + + interesting subject <-- The first direct descendant (that is not a template) is the trigger + + + that you may be interested in! +

+``` + +## Attributes + +| Attribute | Required | Values | Notes | +|---------------------|----------|----------|-----------------------------------------------------------------------------------------------------------------| +| offset | No | | The offset in pixels from the trigger that the tooltip should display | + +## Events + +| Event | Notes | +|-------|-------------------------------| +| show | After the tooltip is visible | +| hide | After the tooltip is hidden | diff --git a/src/tooltip/index.html b/src/tooltip/index.html new file mode 100644 index 0000000..b56b5f6 --- /dev/null +++ b/src/tooltip/index.html @@ -0,0 +1,179 @@ + + + + + + + + Web Component: Tooltip + + + + + + +
+ +
+ + + + + + +

Lorem ipsum dolor sit amet consectetur adipisicing elit. Vero excepturi, esse aperiam omnis voluptatem + et perspiciatis quas nostrum. Modi velit repellat labore illo, laudantium eaque, ea esse tempora earum + natus ullam repudiandae voluptatibus ut perferendis nobis. Nesciunt, possimus accusamus sed dolor + exercitationem repellendus sunt perspiciatis. Nulla saepe assumenda facere. Aliquid et eum accusantium + velit nisi rem, eaque, repudiandae voluptas culpa dolor hic at aperiam dignissimos voluptatum quia, + architecto incidunt! Unde ipsa maiores maxime repellendus, voluptatem assumenda doloribus fugit + necessitatibus ipsam soluta laboriosam voluptas optio a tenetur eveniet nostrum debitis hic, beatae + eos asperiores repellat consectetur animi est dolor? Quis, recusandae neque deserunt asperiores culpa + + + Paragraph link 1 + + exercitationem autem consequatur, sequi saepe blanditiis, laudantium vel? Voluptates pariatur ducimus + corporis magni. Placeat, fugiat, veniam incidunt dolorum numquam itaque non inventore, aliquam laboriosam + dolor mollitia ipsa! Nemo voluptas, animi corrupti facere perspiciatis commodi voluptatibus sint saepe + suscipit? Molestiae temporibus recusandae delectus molestias, ea, tempora unde, sapiente quae reiciendis + suscipit est? Nemo enim eius suscipit possimus eum debitis adipisci officiis, dolor, iste autem delectus + quam illum ut asperiores expedita impedit ad atque repellat aut quisquam doloremque! Nihil magnam harum + + + Paragraph link 2 + + dolores in neque perspiciatis! Velit, repellendus sapiente cumque ullam optio molestias ipsum doloribus + blanditiis. Eum at quibusdam voluptatem fugiat facere omnis ipsam est obcaecati provident laborum dolores + exercitationem aspernatur saepe cumque animi eligendi corrupti, aliquid tenetur. Similique quam accusantium + numquam a, aliquam libero debitis quibusdam consequatur modi doloremque, exercitationem eum temporibus quo + quia velit repellendus, aspernatur eos. Optio exercitationem dolore ipsum alias est quas velit eaque autem + ducim us. Iusto fugit reiciendis nisi fugiat deserunt, quibusdam doloribus. Eligendi a officia quas totam + adipisci minus voluptatibus dignissimos sed nostrum. Consequatur eos est illo tempore odit, adipisci itaque + + + Paragraph link 3 + + magnam, obcaecati ullam possimus ab sint beatae, animi sed mollitia sit perspiciatis eligendi! Tenetur minus + vero soluta amet excepturi sit quia animi. +

+
+ +
+ +
+ + + + + + +
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+
+ +
+ + diff --git a/src/tooltip/index.ts b/src/tooltip/index.ts new file mode 100644 index 0000000..89552c7 --- /dev/null +++ b/src/tooltip/index.ts @@ -0,0 +1,18 @@ +/** + * Styles. + */ +import './style.scss'; + +/** + * Components. + */ +import { TPTooltip } from './tp-tooltip'; +import { TPTooltipTrigger } from './tp-tooltip-trigger'; +import { TPTooltipArrow } from './tp-tooltip-arrow'; + +/** + * Register Components. + */ +customElements.define( 'tp-tooltip', TPTooltip ); +customElements.define( 'tp-tooltip-trigger', TPTooltipTrigger ); +customElements.define( 'tp-tooltip-arrow', TPTooltipArrow ); diff --git a/src/tooltip/style.scss b/src/tooltip/style.scss new file mode 100644 index 0000000..f6e4b09 --- /dev/null +++ b/src/tooltip/style.scss @@ -0,0 +1,41 @@ +tp-tooltip { + border: 1px solid #000; + + &[popover] { + overflow: visible; + margin: 0; + padding: 0; + } + + tp-tooltip-content { + position: relative; + display: block; + z-index: 10; + } + + tp-tooltip-arrow { + position: absolute; + display: block; + z-index: 5; + border: 1px solid #000; + width: 15px; + height: 15px; + background-color: #fff; + + &[position="top"] { + top: -1px; + transform: translateY(-50%) translateX(-50%) rotate(45deg); + border-right: none; + border-bottom: none; + } + + &[position="bottom"] { + bottom: -1px; + transform: translateY(50%) translateX(-50%) rotate(45deg); + border-left: none; + border-top: none; + } + } +} + + diff --git a/src/tooltip/tp-tooltip-arrow.ts b/src/tooltip/tp-tooltip-arrow.ts new file mode 100644 index 0000000..d9dcce4 --- /dev/null +++ b/src/tooltip/tp-tooltip-arrow.ts @@ -0,0 +1,5 @@ +/** + * TP Tooltip Arrow. + */ +export class TPTooltipArrow extends HTMLElement { +} diff --git a/src/tooltip/tp-tooltip-trigger.ts b/src/tooltip/tp-tooltip-trigger.ts new file mode 100644 index 0000000..8a9ae58 --- /dev/null +++ b/src/tooltip/tp-tooltip-trigger.ts @@ -0,0 +1,133 @@ +/** + * Internal dependencies. + */ +import { TPTooltip } from './tp-tooltip'; + +/** + * TP Tooltip Trigger. + */ +export class TPTooltipTrigger extends HTMLElement { + /** + * Constructor. + */ + constructor() { + // Call parent's constructor. + super(); + + // Check if touch device. + if ( navigator.maxTouchPoints > 0 ) { + // Yes it is, toggle tooltip on click. + this.addEventListener( 'click', this.toggleTooltip.bind( this ) ); + } else { + // Else add event listeners for mouse. + this.addEventListener( 'mouseenter', this.showTooltip.bind( this ) ); + this.addEventListener( 'mouseleave', this.hideTooltip.bind( this ) ); + } + + // On window scroll, hide tooltip. + window.addEventListener( 'scroll', this.handleWindowScroll.bind( this ), true ); + } + + /** + * Toggle the tooltip. + */ + toggleTooltip(): void { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + + // Check if we have a tooltip. + if ( '' === tooltipId ) { + // We don't, bail. + return; + } + + // Find the tooltip. + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + + // Check if the tooltip is already shown. + if ( 'yes' === tooltip?.getAttribute( 'show' ) ) { + // It is, hide it. + tooltip?.hide(); + } else { + // It isn't, show it. + tooltip?.setTrigger( this ); + tooltip?.show(); + } + } + + /** + * Show the tooltip. + */ + showTooltip(): void { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + + // Check if we have a tooltip. + if ( '' === tooltipId ) { + // We don't, bail. + return; + } + + // Find and show the tooltip with its content. + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + tooltip?.setTrigger( this ); + tooltip?.show(); + } + + /** + * Hide the tooltip. + */ + hideTooltip(): void { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + + // Check if we have a tooltip. + if ( '' === tooltipId ) { + // We don't, bail. + return; + } + + // Find and hide the tooltip. + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + tooltip?.hide(); + } + + /** + * Get the content of the tooltip. + * + * @return {Node|null} The content of the tooltip. + */ + getContent(): Node | null { + // Find template for content. + const template: HTMLTemplateElement | null = this.querySelector( 'template' ); + + // Check if we found a template. + if ( template ) { + // We did, return its content. + return template.content.cloneNode( true ); + } + + // No template found, return null. + return null; + } + + /** + * Handles the scroll outside of the tooltip. + * + * @param { Event } evt The event object. + */ + handleWindowScroll( evt: Event ) { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + + // If the content was the original target. + if ( evt.target === tooltip ) { + // Do nothing. + return; + } + + // Hide the popover + this.hideTooltip(); + } +} diff --git a/src/tooltip/tp-tooltip.ts b/src/tooltip/tp-tooltip.ts new file mode 100644 index 0000000..9b9b2d7 --- /dev/null +++ b/src/tooltip/tp-tooltip.ts @@ -0,0 +1,175 @@ +/** + * Internal dependencies. + */ +import { TPTooltipTrigger } from './tp-tooltip-trigger'; +import { TPTooltipArrow } from './tp-tooltip-arrow'; + +/** + * TP Tooltip. + */ +export class TPTooltip extends HTMLElement { + /** + * Properties. + */ + protected trigger: TPTooltipTrigger | null = null; + + /** + * Constructor. + */ + constructor() { + // Call parent's constructor. + super(); + + // Make the tooltip a popover. + this.makePopover(); + } + + /** + * Get offset. + */ + get offset(): number { + // Get the offset. + return parseInt( this.getAttribute( 'offset' ) ?? '0' ); + } + + /** + * Set offset. + */ + set offset( offset: number ) { + // Set or remove offset. + if ( ! offset ) { + this.removeAttribute( 'offset' ); + } else { + this.setAttribute( 'offset', offset.toString() ); + } + } + + /** + * Make this tooltip a popover, if it isn't already. + */ + makePopover(): void { + // Check if this isn't already a popover. + if ( ! this.getAttribute( 'popover' ) ) { + this.setAttribute( 'popover', '' ); + } + } + + /** + * Set the trigger. + * + * @param {HTMLElement} trigger The trigger node. + */ + setTrigger( trigger: TPTooltipTrigger ): void { + // Set the trigger. + this.trigger = trigger; + } + + /** + * Set the content for our tooltip. + */ + setContent(): void { + // Get content. + const content: Node | null = this.trigger?.getContent() ?? null; + + // Check if we have content. + if ( content ) { + // Yes, replace slot's children with new content. + this.querySelector( 'slot' )?.replaceChildren( content ); + } + } + + /** + * Set the position of the tooltip. + */ + setPosition(): void { + // Do we have a trigger? + if ( ! this.trigger ) { + // We don't, bail! + return; + } + + // Get width and height of this tooltip. + const { height: tooltipHeight, width: tooltipWidth } = this.getBoundingClientRect(); + + // Get position and coordinates of the trigger. + const { x: triggerLeftPosition, y: triggerTopPosition, width: triggerWidth, height: triggerHeight } = this.trigger.getBoundingClientRect(); + + // Trigger center from left edge of screen. + const triggerCenterPosition = triggerLeftPosition + ( triggerWidth / 2 ); + + // Get arrow dimensions. + let arrowHeight: number = 0; + const arrow: TPTooltipArrow | null = this.querySelector( 'tp-tooltip-arrow' ); + + // Check if we have an arrow. + if ( arrow ) { + ( { height: arrowHeight } = arrow.getBoundingClientRect() ); + } + + // Determine the vertical position of this tooltip. + if ( triggerTopPosition > tooltipHeight + this.offset + arrowHeight ) { + // There is enough space on top of trigger element, so place popover above the trigger element. + this.style.marginTop = `${ triggerTopPosition - tooltipHeight - this.offset - ( arrowHeight / 2 ) }px`; + + // Set arrow placement on bottom of popover + arrow?.setAttribute( 'position', 'bottom' ); + } else { + // There is not enough space on top of trigger element, so place popover below the trigger element. + this.style.marginTop = `${ triggerTopPosition + triggerHeight + this.offset + ( arrowHeight / 2 ) }px`; + + // Set arrow placement on top of popover + arrow?.setAttribute( 'position', 'top' ); + } + + // Determine the horizontal position of this tooltip. + if ( triggerCenterPosition < ( tooltipWidth / 2 ) ) { + // There is not enough space on left of trigger element, so place popover at the left edge of the screen. + this.style.marginLeft = '0px'; + } else if ( window.innerWidth - triggerCenterPosition < ( tooltipWidth / 2 ) ) { + // There is not enough space on right of trigger element, so place popover at the right edge of the screen. + this.style.marginLeft = `${ window.innerWidth - tooltipWidth }px`; + } else { + // There is enough space on both sides of trigger element, so place popover at the center of the trigger element. + this.style.marginLeft = `${ triggerLeftPosition + ( triggerWidth / 2 ) - ( tooltipWidth / 2 ) }px`; + } + + // Get left position of the tooltip. + const { left: tooltipLeftPosition } = this.getBoundingClientRect(); + + // Percentage the arrow should be moved from left edge of the tooltip. + const arrowPosition = ( ( triggerCenterPosition - tooltipLeftPosition ) / tooltipWidth ) * 100; + + // Set the arrow position. + if ( arrow ) { + arrow.style.left = `${ arrowPosition }%`; + } + } + + /** + * Show the tooltip. + */ + show(): void { + // Position tooltip and show it. + this.setContent(); + + // Show the tooltip. + this.showPopover(); + this.setPosition(); + this.setAttribute( 'show', 'yes' ); + + // Trigger event. + this.dispatchEvent( new CustomEvent( 'show' ) ); + } + + /** + * Hide the tooltip. + */ + hide(): void { + // Hide the tooltip. + this.hidePopover(); + this.removeAttribute( 'show' ); + + // Trigger event. + this.dispatchEvent( new CustomEvent( 'hide' ) ); + } +} diff --git a/webpack.config.js b/webpack.config.js index a347deb..655bceb 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -85,6 +85,7 @@ module.exports = ( env ) => { lightbox: './src/lightbox/index.ts', 'toggle-attribute': './src/toggle-attribute/index.ts', 'number-spinner': './src/number-spinner/index.ts', + tooltip: './src/tooltip/index.ts', }, module: { rules: [