Skip to content

Commit

Permalink
feat(tooltip): implement component
Browse files Browse the repository at this point in the history
  • Loading branch information
dpellier committed Jul 29, 2024
1 parent ff0cecd commit 25a43d3
Show file tree
Hide file tree
Showing 39 changed files with 1,452 additions and 3 deletions.
1 change: 1 addition & 0 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": ["stylelint-config-concentric-order", "stylelint-config-standard-scss"],
"rules": {
"alpha-value-notation": "number",
"color-function-notation": "legacy",
"color-named": "never",
"hue-degree-notation": "number",
Expand Down
1 change: 1 addition & 0 deletions packages/ods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"lint:scss": "stylelint 'src/style/*.scss'"
},
"dependencies": {
"@floating-ui/dom": "1.6.3",
"@stencil/core": "4.12.2"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/ods/react/tests/_app/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const componentNames = [
'text',
'icon',
'skeleton',
'tooltip',
//--generator-anchor--
];

Expand Down
20 changes: 20 additions & 0 deletions packages/ods/react/tests/_app/src/components/ods-tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react-dom/client';
import { OdsTooltip } from 'ods-components-react';

const Tooltip = () => {
return (
<>
<div id="trigger">
Trigger
</div>

<OdsTooltip position="bottom-start"
triggerId="trigger"
withArrow={ true }>
Tooltip content
</OdsTooltip>
</>
);
};

export default Tooltip;
31 changes: 31 additions & 0 deletions packages/ods/react/tests/e2e/ods-tooltip.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Page } from 'puppeteer';
import { goToComponentPage, setupBrowser } from '../setup';

describe('ods-tooltip react', () => {
const setup = setupBrowser();
let page: Page;

async function isTooltipVisible(): Promise<boolean> {
return page.evaluate(() => {
return document.querySelector<HTMLElement>('ods-tooltip')?.style.display === 'block';
});
}

beforeAll(async () => {
page = setup().page;
});

beforeEach(async () => {
await goToComponentPage(page, 'ods-tooltip');
});

it('render the component correctly', async () => {
const trigger = await page.$('#trigger');

expect(await isTooltipVisible()).toBe(false);

await trigger?.hover();

expect(await isTooltipVisible()).toBe(true);
});
});
2 changes: 1 addition & 1 deletion packages/ods/react/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function setupBrowser() {
beforeAll(async () => {
browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
headless: true
headless: true,
});
page = await browser.newPage();
});
Expand Down
1 change: 1 addition & 0 deletions packages/ods/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './spinner/src';
export * from './text/src';
export * from './icon/src';
export * from './skeleton/src';
export * from './tooltip/src';
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
## Usage changes

The tooltip usage has changed a bit regarding the trigger element declaration.

Previously, the trigger element is declared inside the web-component as its main slot content.

The tooltip content is the declared using a `osds-tooltip-content` with the slot `tooltip-content` parameter.

This was causing an issue of double focus when the trigger used was a focusable element.

To fix that and give a bit more flexibility about how you organize your DOM, the trigger is now declared outside of the web-component.

The only requirement is to give this element an `id` so that the tooltip can get attached to it using the new attribute `trigger-id`.

Here is an example of the previous declaration:

```html
<ods-tooltip>
<div>
Hover me
</div>

<osds-tooltip-content slot="tooltip-content">
Tooltip content
</osds-tooltip-content>
</ods-tooltip>
```

The same result would now be achieved using:
```html
<div id="tooltip-trigger">
Hover me
</div>

<ods-tooltip trigger-id="tooltip-trigger">
Tooltip content
</ods-tooltip>
```

## Attributes changes

`position` <img src="https://img.shields.io/badge/new-008000" />

New attribute (optional).

You can choose a specific position for the tooltip. In case there is not enough space, it will automatically fallback
to another position where the whole content can be displayed.

`triggerId` <img src="https://img.shields.io/badge/new-008000" />

New attribute (required).

Id of the HTML element that serves as the tooltip trigger.

`variant` <img src="https://img.shields.io/badge/removed-FF0000" />

Has been removed.

You can use the new `with-arrow` attribute to render either the standard or tip previous variant.

`with-arrow` <img src="https://img.shields.io/badge/removed-FF0000" />

New attribute (optional).

Set it to `true` if you want to add an arrow to the tooltip.

## Migration examples

Basic tooltip:
```html
<ods-tooltip>
<div>
Hover me
</div>

<osds-tooltip-content slot="tooltip-content">
Tooltip content
</osds-tooltip-content>
</ods-tooltip>

<!-- is now -->

<div id="tooltip-trigger">
Hover me
</div>

<ods-tooltip trigger-id="tooltip-trigger">
Tooltip content
</ods-tooltip>
```

Arrow tooltip:
```html
<ods-tooltip variant="tip">
<div>
Hover me
</div>

<osds-tooltip-content slot="tooltip-content">
Tooltip content
</osds-tooltip-content>
</ods-tooltip>

<!-- is now -->

<div id="tooltip-trigger">
Hover me
</div>

<ods-tooltip trigger-id="tooltip-trigger"
with-arrow>
Tooltip content
</ods-tooltip>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Canvas } from '@storybook/blocks'

Tooltip component is used to display contextual information when the user hovers over or focus on an UI element in a page:

<Canvas>
<div style={{ height: '300px', display: 'inline-flex'}}>
<div>
Hover me
</div>

<ods-tooltip>
Tooltip content
</ods-tooltip>
</div>
</Canvas>

<div>
<a href="#"
target="_blank">
ZEROHEIGHT - SKELETON DESIGN GUIDELINES &nbsp;
</a>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Content of this file will be auto-generated
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Markdown } from '@storybook/blocks';
import Specs from './spec.md';

<Markdown>{ Specs }</Markdown>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Canvas } from '@storybook/blocks';

## Default

<Canvas sourceState="none">
<div>
Hover me
</div>

<ods-tooltip>
Tooltip content
</ods-tooltip>
</Canvas>

```html
<div>
Hover me
</div>

<ods-tooltip>
Tooltip content
</ods-tooltip>
```

## With an arrow tip

<Canvas sourceState="none">
<div>
Hover me
</div>

<ods-tooltip with-arrow>
Tooltip content
</ods-tooltip>
</Canvas>

```html
<div>
Hover me
</div>

<ods-tooltip with-arrow>
Tooltip content
</ods-tooltip>
```
19 changes: 19 additions & 0 deletions packages/ods/src/components/tooltip/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@ovhcloud/ods-component-tooltip",
"version": "17.1.0",
"private": true,
"description": "ODS Tooltip component",
"main": "dist/index.cjs.js",
"collection": "dist/collection/collection-manifest.json",
"scripts": {
"clean": "rimraf .stencil coverage dist docs-api www",
"doc": "typedoc --pretty --plugin ../../../scripts/typedoc-plugin-decorator.js && node ../../../scripts/generate-typedoc-md.js",
"lint:scss": "stylelint 'src/components/**/*.scss'",
"lint:ts": "eslint 'src/**/*.{js,ts,tsx}'",
"start": "stencil build --dev --watch --serve",
"test:e2e": "stencil test --e2e --config stencil.config.ts",
"test:e2e:ci": "tsc --noEmit && stencil test --e2e --ci --config stencil.config.ts",
"test:spec": "stencil test --spec --config stencil.config.ts --coverage",
"test:spec:ci": "tsc --noEmit && stencil test --config stencil.config.ts --spec --ci --coverage"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@import '../../../../../style/tooltip';

:host(.ods-tooltip) {
@include ods-tooltip;

display: none;
position: absolute;
top: 0;
left: 0;
}

.ods-tooltip {
&__arrow {
position: absolute;
transform: rotate(45deg);
background: inherit;
width: 8px;
height: 8px;

&--hidden {
display: none;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component, Element, type FunctionalComponent, Host, Method, Prop, h } from '@stencil/core';
import { ODS_TOOLTIP_POSITION, type OdsTooltipPosition } from '../../constants/tooltip-position';
import { hideTooltip, showTooltip } from '../../controller/ods-tooltip';

@Component({
shadow: true,
styleUrl: 'ods-tooltip.scss',
tag: 'ods-tooltip',
})
export class OdsTooltip {
private arrowElement!: HTMLElement;
private triggerElement?: HTMLElement | null;
private cleanUpCallback: () => void = () => {};

@Element() el!: HTMLElement;

@Prop({ reflect: true }) public position: OdsTooltipPosition = ODS_TOOLTIP_POSITION.top;
@Prop({ reflect: true }) public triggerId!: string;
@Prop({ reflect: true }) public withArrow?: boolean;

connectedCallback(): void {
this.triggerElement = document.querySelector<HTMLElement>(`#${this.triggerId}`);

if (!this.triggerElement) {
console.warn(`[ods-tooltip] Unable to find trigger DOM element with id: ${this.triggerId}`);
} else {
this.triggerElement.addEventListener('blur', () => this.hide());
this.triggerElement.addEventListener('focus', () => this.show());
this.triggerElement.addEventListener('mouseenter', () => this.show());
this.triggerElement.addEventListener('mouseleave', () => this.hide());
}
}

disconnectedCallback() : void {
this.triggerElement?.removeEventListener('blur', () => this.hide());
this.triggerElement?.removeEventListener('focus', () => this.show());
this.triggerElement?.removeEventListener('mouseenter', () => this.show());
this.triggerElement?.removeEventListener('mouseleave', () => this.hide());
}

@Method()
async hide(): Promise<void> {
hideTooltip(this.el, this.cleanUpCallback);
}

@Method()
async show(): Promise<void> {
this.cleanUpCallback = showTooltip(this.position, {
arrow: this.arrowElement,
popper: this.el,
trigger: this.triggerElement,
});
}

render(): FunctionalComponent {
return (
<Host class="ods-tooltip">
<slot></slot>

<div
class={{
'ods-tooltip__arrow': true,
'ods-tooltip__arrow--hidden': !this.withArrow,
}}
ref={ (el?: HTMLElement) => el && (this.arrowElement = el) }>
</div>
</Host>
);
}
}
Loading

0 comments on commit 25a43d3

Please sign in to comment.