Skip to content

Commit

Permalink
feat(components): tooltips (#1879)
Browse files Browse the repository at this point in the history
Adds a `post-tooltip` component.

- Why `[popover]`?: using the popover attribute (with polyfill) has
several advantages. In supporting browsers, the tooltip will be lifted
to the top-layer, ignoring `overflow: hidden` and the `z-index` order.
This ensures that tooltips are always displayed on top of everything and
never cut off. Until all browsers support the popover API (ETA is end of
2023), a polyfill without these qualities (but some decent standards) is
needed.
- Why `[data-tooltip-trigger]`? This is a custom attribute in place of
`popovertargetaction`, an attribute that can be used to trigger a
popover, but reacts to clicks only. In this situation we want to show
the popover on hover, focus and long-press. This behavior is in
consideration with `popovertargetaction="interest"`, but is not
implemented at the moment. To be able to patch this functionality
without interference from clicks, there is no `popovertargetaction` on
the trigger element.
- floating-ui: this component uses
[floating-ui](https://floating-ui.com/docs/getting-started), the
successor to `@popperjs/core`, for positioning. This low-level library
can be used to dynamically position the tooltip and the corresponding
arrow and can be customised in every imaginable way while leaving a
pretty small footprint.
- No snapshot tests: not sure how to test something that only shows up
on user interaction
- No animation: had some trouble implementing an animation that was
compatible with the polyfill as well as the default API. Suggestion:
wait till Design provides a concept and the popover API is standardised

---------

Co-authored-by: imagoiq <[email protected]>
  • Loading branch information
gfellerph and imagoiq authored Sep 26, 2023
1 parent 7d8038c commit 70cd479
Show file tree
Hide file tree
Showing 14 changed files with 689 additions and 308 deletions.
6 changes: 6 additions & 0 deletions .changeset/brave-vans-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@swisspost/design-system-documentation': minor
'@swisspost/design-system-components': minor
---

Added the `post-tooltip` component.
3 changes: 2 additions & 1 deletion packages/components/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@stencil-community/required-jsdoc": "error",
"@stencil-community/reserved-member-names": "error",
"@stencil-community/single-export": "error",
"@stencil-community/strict-mutable": "error"
"@stencil-community/strict-mutable": "error",
"react/jsx-no-bind": "off"
}
}
54 changes: 54 additions & 0 deletions packages/components/cypress/e2e/tooltip.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
describe('tooltips', () => {
describe('default', () => {
beforeEach(() => {
cy.getComponent('tooltip', 'multiple');
cy.get('button[data-tooltip-target="tooltip-multiple"]:first-of-type').as('target1');
cy.get('button[data-tooltip-target="tooltip-multiple"]:last-of-type').as('target2');
cy.get('#tooltip-multiple').shadow().find('div[popover]').as('tooltip');
});

it('should display a tooltip', () => {
cy.get('@tooltip').should('not.be.visible');
cy.get('@target2').focus();
cy.get('@tooltip').should('be.visible');
cy.get('@target2').blur();
cy.get('@tooltip').should('not.be.visible');
});

it('tooltip placement right', () => {
cy.get('#tooltip-multiple').invoke('attr', 'placement', 'right');
cy.get('@target2').focus();
cy.wait(10);
cy.get('@tooltip')
.should('have.css', 'left')
.then((v: any) => {
expect(parseInt(v)).to.be.greaterThan(150);
});
});
});

describe('non-focusable element', () => {
beforeEach(() => {
cy.getComponent('tooltip', 'non-focusable');
cy.get('cite[data-tooltip-target="tooltip-non-focusable"]').as('target');
});

it('should add tabindex', () => {
cy.get('@target').should('have.attr', 'tabindex').should('eq', '0');
});
});

describe('aria', () => {
beforeEach(() => {
cy.getComponent('tooltip', 'multiple');
cy.get('button[data-tooltip-target="tooltip-multiple"]:first-of-type').as('target1');
cy.get('@target1').invoke('attr', 'aria-describedby', 'existing-value');
});

it('should append aria-describedby without deleting existing values', () => {
cy.get('@target1')
.should('have.attr', 'aria-describedby')
.should('eq', 'existing-value tooltip-multiple');
});
});
});
8 changes: 6 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@
"lint": "eslint src/**/*{.ts,.tsx}"
},
"dependencies": {
"@floating-ui/dom": "1.5.1",
"@oddbird/popover-polyfill": "0.2.2",
"@stencil/core": "3.4.2",
"@swisspost/design-system-styles": "workspace:6.4.0"
"@swisspost/design-system-styles": "workspace:6.4.0",
"ally.js": "1.4.1",
"long-press-event": "2.4.6"
},
"devDependencies": {
"@percy/cli": "1.27.2",
"@percy/cypress": "3.1.2",
"@stencil-community/eslint-plugin": "^0.5.0",
"@stencil-community/eslint-plugin": "0.5.0",
"@stencil/react-output-target": "0.5.3",
"@stencil/sass": "3.0.5",
"@types/jest": "27.5.2",
Expand Down
48 changes: 48 additions & 0 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { BackgroundColor } from "./components/post-tooltip/types";
import { Placement } from "@floating-ui/dom";
export { BackgroundColor } from "./components/post-tooltip/types";
export { Placement } from "@floating-ui/dom";
export namespace Components {
interface PostCollapsible {
/**
Expand Down Expand Up @@ -75,6 +79,31 @@ export namespace Components {
*/
"show": (panelName: string) => Promise<void>;
}
interface PostTooltip {
/**
* Defines the background color of the tooltip. Choose the one that provides the best contrast in your scenario.
*/
"backgroundColor"?: BackgroundColor;
/**
* Programmatically hide this tooltip
*/
"hide": () => Promise<void>;
/**
* 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;
/**
* Programmatically display the tooltip
* @param target An element with [data-tooltip-target="id"] where the tooltip should be shown
*/
"show": (target: HTMLElement) => Promise<void>;
/**
* Toggle tooltip display
* @param target An element with [data-tooltip-target="id"] where the tooltip should be shown
* @param force Pass true to always show or false to always hide
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
}
}
export interface PostTabsCustomEvent<T> extends CustomEvent<T> {
detail: T;
Expand Down Expand Up @@ -114,12 +143,19 @@ declare global {
prototype: HTMLPostTabsElement;
new (): HTMLPostTabsElement;
};
interface HTMLPostTooltipElement extends Components.PostTooltip, HTMLStencilElement {
}
var HTMLPostTooltipElement: {
prototype: HTMLPostTooltipElement;
new (): HTMLPostTooltipElement;
};
interface HTMLElementTagNameMap {
"post-collapsible": HTMLPostCollapsibleElement;
"post-icon": HTMLPostIconElement;
"post-tab-header": HTMLPostTabHeaderElement;
"post-tab-panel": HTMLPostTabPanelElement;
"post-tabs": HTMLPostTabsElement;
"post-tooltip": HTMLPostTooltipElement;
}
}
declare namespace LocalJSX {
Expand Down Expand Up @@ -188,12 +224,23 @@ declare namespace LocalJSX {
*/
"onTabChange"?: (event: PostTabsCustomEvent<HTMLPostTabPanelElement['name']>) => void;
}
interface PostTooltip {
/**
* Defines the background color of the tooltip. Choose the one that provides the best contrast in your scenario.
*/
"backgroundColor"?: BackgroundColor;
/**
* 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 IntrinsicElements {
"post-collapsible": PostCollapsible;
"post-icon": PostIcon;
"post-tab-header": PostTabHeader;
"post-tab-panel": PostTabPanel;
"post-tabs": PostTabs;
"post-tooltip": PostTooltip;
}
}
export { LocalJSX as JSX };
Expand All @@ -208,6 +255,7 @@ declare module "@stencil/core" {
"post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes<HTMLPostTabHeaderElement>;
"post-tab-panel": LocalJSX.PostTabPanel & JSXBase.HTMLAttributes<HTMLPostTabPanelElement>;
"post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes<HTMLPostTabsElement>;
"post-tooltip": LocalJSX.PostTooltip & JSXBase.HTMLAttributes<HTMLPostTooltipElement>;
}
}
}
59 changes: 59 additions & 0 deletions packages/components/src/components/post-tooltip/post-tooltip.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@use 'sass:meta';
@use 'sass:math';
@use '@swisspost/design-system-styles/variables/color';
@use '@swisspost/design-system-styles/variables/commons';
@use '@swisspost/design-system-styles/variables/spacing';
@use '@swisspost/design-system-styles/mixins/color' as color-mx;

// Puts polyfilled styles in a separate layer so they are easy to override
// Can be removed as soon as popover is supported by all major browsers
// https://caniuse.com/?search=popover
@layer polyfill {
@include meta.load-css('@oddbird/popover-polyfill/dist/popover.css');
}

:host {
// Sets default background color
@include color-mx.colored-background(color.$gray-80);
}

div {
position: absolute;
z-index: commons.$zindex-tooltip;

width: max-content;
max-width: 30ch;
margin: 0;
padding: spacing.$size-micro spacing.$size-mini;

color: inherit;
background-color: inherit;
border-color: transparent; // Keeping the default border for HCM
border-radius: commons.$border-radius;

// Keeps the little arrow visible
overflow: visible;

// Prevents instantly closing tooltips because the opening tooltip opens under the cursor and the trigger gets a mouseleave
pointer-events: none;
}

.arrow {
position: absolute;
// Diagonale of 16px -> 1rem -> 1/1.41 = ~0.7
// https://www.omnicalculator.com/math/square-diagonal?c=CHF&v=hide:0,diagonal:16!cm
width: math.div(spacing.$spacer, math.sqrt(2));
aspect-ratio: 1/1;
background-color: inherit;
rotate: 45deg;
pointer-events: none;
z-index: -1;

// High contrast mode borders
border-right: 2px solid transparent;
border-bottom: 2px solid transparent;
}

.bg-yellow {
@include color-mx.colored-background(color.$yellow);
}
Loading

0 comments on commit 70cd479

Please sign in to comment.