Skip to content

Commit

Permalink
feat(tooltip)!: add post-tooltip-trigger
Browse files Browse the repository at this point in the history
In order to get rid of mutation observers on attributes, a new element to wrap the trigger was added for the tooltip. This enables easier lifecycle management and more precise event listeners with less overhead.
  • Loading branch information
gfellerph committed Jun 28, 2024
1 parent 10b906c commit 628cd1e
Show file tree
Hide file tree
Showing 14 changed files with 283 additions and 190 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-shoes-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swisspost/design-system-components': major
---

Removed the arrow option for the `<post-tooltip>` element, the arrow is always shown for tooltips.
5 changes: 5 additions & 0 deletions .changeset/unlucky-books-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swisspost/design-system-components': major
---

Updated the trigger for the `<post-tooltip>`. Instead of using an attribute to link a trigger element with the tooltip, a new `<post-tooltip-trigger>` element is available for wrapping the element that should display the tooltip.
37 changes: 33 additions & 4 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export namespace Components {
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
}
interface PostPopovercontainer {
/**
* Animation style
*/
"animation"?: 'pop-in';
/**
* Wheter or not to display a little pointer arrow
*/
Expand Down Expand Up @@ -277,9 +281,9 @@ export namespace Components {
}
interface PostTooltip {
/**
* Wheter or not to display a little pointer arrow
* Choose a tooltip animation
*/
"arrow"?: boolean;
"animation"?: 'pop-in' | null;
/**
* Programmatically hide this tooltip
*/
Expand All @@ -300,6 +304,12 @@ export namespace Components {
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
}
interface PostTooltipTrigger {
/**
* Link the trigger to a tooltip with this id
*/
"for": string;
}
}
export interface PostAlertCustomEvent<T> extends CustomEvent<T> {
detail: T;
Expand Down Expand Up @@ -484,6 +494,12 @@ declare global {
prototype: HTMLPostTooltipElement;
new (): HTMLPostTooltipElement;
};
interface HTMLPostTooltipTriggerElement extends Components.PostTooltipTrigger, HTMLStencilElement {
}
var HTMLPostTooltipTriggerElement: {
prototype: HTMLPostTooltipTriggerElement;
new (): HTMLPostTooltipTriggerElement;
};
interface HTMLElementTagNameMap {
"post-accordion": HTMLPostAccordionElement;
"post-accordion-item": HTMLPostAccordionItemElement;
Expand All @@ -499,6 +515,7 @@ declare global {
"post-tabs": HTMLPostTabsElement;
"post-tag": HTMLPostTagElement;
"post-tooltip": HTMLPostTooltipElement;
"post-tooltip-trigger": HTMLPostTooltipTriggerElement;
}
}
declare namespace LocalJSX {
Expand Down Expand Up @@ -656,6 +673,10 @@ declare namespace LocalJSX {
"placement"?: Placement;
}
interface PostPopovercontainer {
/**
* Animation style
*/
"animation"?: 'pop-in';
/**
* Wheter or not to display a little pointer arrow
*/
Expand Down Expand Up @@ -733,14 +754,20 @@ declare namespace LocalJSX {
}
interface PostTooltip {
/**
* Wheter or not to display a little pointer arrow
* Choose a tooltip animation
*/
"arrow"?: boolean;
"animation"?: 'pop-in' | null;
/**
* Defines the placement of the tooltip according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Tooltips are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
*/
"placement"?: Placement;
}
interface PostTooltipTrigger {
/**
* Link the trigger to a tooltip with this id
*/
"for"?: string;
}
interface IntrinsicElements {
"post-accordion": PostAccordion;
"post-accordion-item": PostAccordionItem;
Expand All @@ -756,6 +783,7 @@ declare namespace LocalJSX {
"post-tabs": PostTabs;
"post-tag": PostTag;
"post-tooltip": PostTooltip;
"post-tooltip-trigger": PostTooltipTrigger;
}
}
export { LocalJSX as JSX };
Expand All @@ -782,6 +810,7 @@ declare module "@stencil/core" {
"post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes<HTMLPostTabsElement>;
"post-tag": LocalJSX.PostTag & JSXBase.HTMLAttributes<HTMLPostTagElement>;
"post-tooltip": LocalJSX.PostTooltip & JSXBase.HTMLAttributes<HTMLPostTooltipElement>;
"post-tooltip-trigger": LocalJSX.PostTooltipTrigger & JSXBase.HTMLAttributes<HTMLPostTooltipTriggerElement>;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@use '@swisspost/design-system-styles/variables/commons';
@use '@swisspost/design-system-styles/variables/spacing';
@use '@swisspost/design-system-styles/mixins/color' as color-mx;
@use '@swisspost/design-system-styles/mixins/animation' as animation-mx;
@use '@swisspost/design-system-styles/mixins/elevation' as elevation-mx;
@use '@swisspost/design-system-styles/functions/color' as color-fn;

Expand Down Expand Up @@ -69,3 +70,13 @@
border-bottom: 2px solid transparent;
}
}

.popover[data-animation='pop-in'] {
@include animation-mx.top-layer-pop-in(0.25s, ':popover-open');

@media (prefers-reduced-motion) {
& {
--transition-duration: 0s;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export class PostPopovercontainer {
*/
@Prop() readonly placement?: Placement = 'top';

/**
* Animation style
*/
@Prop() readonly animation?: 'pop-in' = null;

/**
* Wheter or not to display a little pointer arrow
*/
Expand Down Expand Up @@ -98,10 +103,8 @@ export class PostPopovercontainer {
*/
@Method()
async hide() {
if (!this.toggleTimeoutId) {
this.eventTarget = null;
this.popoverRef.hidePopover();
}
this.eventTarget = null;
this.popoverRef.hidePopover();
}

/**
Expand All @@ -111,13 +114,9 @@ export class PostPopovercontainer {
*/
@Method()
async toggle(target: HTMLElement, force?: boolean): Promise<boolean> {
// Prevent instant double toggle
if (!this.toggleTimeoutId) {
this.eventTarget = target;
this.calculatePosition();
this.popoverRef.togglePopover(force);
this.toggleTimeoutId = null;
}
this.eventTarget = target;
this.calculatePosition();
this.popoverRef.togglePopover(force);
return this.popoverRef.matches(':popover-open');
}

Expand All @@ -128,7 +127,6 @@ export class PostPopovercontainer {
* @param e ToggleEvent
*/
private handleToggle(e: ToggleEvent) {
this.toggleTimeoutId = window.setTimeout(() => (this.toggleTimeoutId = null), 10);
const isOpen = e.newState === 'open';
if (isOpen) {
this.startAutoupdates();
Expand Down Expand Up @@ -173,7 +171,7 @@ export class PostPopovercontainer {
offset(this.arrow ? 12 : 8), // 4px outside of element to account for focus outline + ~arrow size
];

if (this.arrow) {
if (this.arrow && this.arrowRef) {
middleware.push(arrow({ element: this.arrowRef, padding: 8 }));
}

Expand All @@ -193,7 +191,7 @@ export class PostPopovercontainer {
this.popoverRef.style.top = `${y}px`;

// Arrow
if (this.arrow) {
if (this.arrow && this.arrowRef) {
// Tutorial: https://codesandbox.io/s/mystifying-kare-ee3hmh?file=/src/index.js
const side = currentPlacement.split('-')[0];
const { x: arrowX, y: arrowY } = middlewareData.arrow;
Expand All @@ -217,6 +215,7 @@ export class PostPopovercontainer {
<Host data-version={version}>
<div
class="popover"
data-animation={this.animation}
part="popover"
ref={(el: HTMLDivElement & PostPopoverElement) => (this.popoverRef = el)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

| Property | Attribute | Description | Type | Default |
| ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `animation` | `animation` | Animation style | `"pop-in"` | `null` |
| `arrow` | `arrow` | Wheter or not to display a little pointer arrow | `boolean` | `false` |
| `placement` | `placement` | Defines the placement of the tooltip according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Tooltips are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries. | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'top'` |

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component, Element, Prop } from '@stencil/core';
import { isFocusable } from '../../utils/is-focusable';
import { version } from '@root/package.json';
import 'long-press-event';

@Component({
tag: 'post-tooltip-trigger',
})
export class PostTooltipTrigger {
/**
* Link the trigger to a tooltip with this id
*/
@Prop() for: string;

@Element() host: HTMLPostTooltipTriggerElement;

private trigger: HTMLElement;
private localInterestHandler;
private localInterestLostHandler;

constructor() {
this.localInterestHandler = this.interestHandler.bind(this);
this.localInterestLostHandler = this.interestLostHandler.bind(this);
}

componentDidLoad() {
this.host.setAttribute('data-version', version);

if (this.host?.children.length > 0 && this.host.children[0].nodeType === 1) {
this.trigger = this.host.children[0] as HTMLElement;
} else {
this.trigger = this.host;
}

// Ensure trigger is focusable
if (!isFocusable(this.trigger)) {
this.trigger.setAttribute('tabindex', '0');
}

// Add tooltip to aria-describedby
const describedBy = this.trigger.getAttribute('aria-describedby');
if (!describedBy?.includes(this.for)) {
const newDescribedBy = describedBy ? `${describedBy} ${this.for}` : this.for;
this.trigger.setAttribute('aria-describedby', newDescribedBy);
}

this.host.addEventListener('pointerover', this.localInterestHandler);
this.host.addEventListener('pointerout', this.localInterestLostHandler);
this.host.addEventListener('focusin', this.localInterestHandler);
this.host.addEventListener('focusout', this.localInterestLostHandler);
this.host.addEventListener('long-press', this.localInterestHandler);
}

private get tooltip(): HTMLPostTooltipElement | null {
const ref = document.getElementById(this.for);
if (ref && ref.tagName === 'POST-TOOLTIP') {
return ref as HTMLPostTooltipElement;
}

return null;
}

private interestHandler() {
this.tooltip?.show(this.trigger);
}

private interestLostHandler() {
this.tooltip?.hide();
}
}
17 changes: 17 additions & 0 deletions packages/components/src/components/post-tooltip-trigger/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# post-tooltip-trigger



<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| -------- | --------- | ------------------------------------------ | -------- | ----------- |
| `for` | `for` | Link the trigger to a tooltip with this id | `string` | `undefined` |


----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
Loading

0 comments on commit 628cd1e

Please sign in to comment.