Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tooltip): a few issues #1165

Merged
merged 18 commits into from
Aug 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions packages/components/src/components/tooltip/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@

## Properties

| Property | Attribute | Description | Type | Default |
| ------------- | -------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `arrowOffset` | `arrow-offset` | (optional) How much of the arrow element is "hidden" | `number` | `-4` |
| `content` | `content` | (optional) The content of the Tooltip supporting Text only | `string` | `''` |
| `disabled` | `disabled` | (optional) Disable Tooltip | `boolean` | `false` |
| `distance` | `distance` | (optional) Distance of the Tooltip from the Target Object (related to the `placement`) | `number` | `10` |
| `flip` | `flip` | (optional) Switching the flip option of the tooltip on and off | `boolean` | `true` |
| `open` | `open` | (optional) Set the Tooltip to open per default (will still be closed on closing Events) | `boolean` | `false` |
| `placement` | `placement` | (optional) Position of the Tooltip on the Object | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'top'` |
| `styles` | `styles` | (optional) Injected CSS styles | `string` | `undefined` |
| `trigger` | `trigger` | (optional) Set custom trigger Event selection | `string` | `'hover focus'` |
| Property | Attribute | Description | Type | Default |
| -------------- | --------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `arrowOffset` | `arrow-offset` | (optional) How much of the arrow element is "hidden" | `number` | `-4` |
| `arrowPadding` | `arrow-padding` | (optional) Padding between the arrow and the edges of the tooltip | `number` | `8` |
| `content` | `content` | (optional) The content of the Tooltip, supporting text only | `string` | `''` |
| `disabled` | `disabled` | (optional) Disable the tooltip | `boolean` | `false` |
| `distance` | `distance` | (optional) Tooltip distance from the target element (related to `placement`) | `number` | `10` |
| `flip` | `flip` | (optional) Switching the flip option of the tooltip on and off | `boolean` | `true` |
| `opened` | `opened` | (optional) Set the tooltip to opened by default (will still be closed on closing events) | `boolean` | `false` |
| `placement` | `placement` | (optional) Position of the Tooltip around the trigger element | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'top'` |
| `styles` | `styles` | (optional) Injected CSS styles | `string` | `undefined` |
| `trigger` | `trigger` | (optional) Set custom trigger event (hover, focus, click) | `string` | `'hover focus'` |


## Events
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/components/tooltip/tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,17 @@
line-height: var(--line-height);
padding: var(--spacing);
border-radius: var(--radius);
transition-property: opacity;
transition-duration: var(--transition-duration-show);
transition-timing-function: var(--transition-timing-function-show);
}

[part='tooltip'][aria-hidden='true'] {
opacity: 0;
transition-property: opacity;
transition-delay: var(--transition-delay-hide);
transition-duration: var(--transition-duration-hide);
transition-timing-function: var(--transition-timing-function-hide);
pointer-events: none;
}

[part='trigger'] {
Expand Down
190 changes: 113 additions & 77 deletions packages/components/src/components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@stencil/core';
import { computePosition, offset, flip, shift, arrow } from '@floating-ui/dom';
import { isClickOutside } from '../../utils/utils';
import statusNote from '../../utils/status-note';

let id = 0;

Expand All @@ -34,11 +35,13 @@ let id = 0;
})
export class Tooltip {
componentId = `tooltip-${++id}`;
@Element() hostEl: HTMLElement;
/** (optional) The content of the Tooltip supporting Text only */
@Prop() content = '';
/** (optional) Position of the Tooltip on the Object */
@Prop() placement:

@Element() hostElement: HTMLElement;

/** (optional) The content of the Tooltip, supporting text only */
@Prop() content? = '';
/** (optional) Position of the Tooltip around the trigger element */
@Prop() placement?:
| 'top'
| 'top-start'
| 'top-end'
Expand All @@ -51,20 +54,23 @@ export class Tooltip {
| 'left'
| 'left-start'
| 'left-end' = 'top';
/** (optional) Disable Tooltip */
@Prop() disabled = false;
/** (optional) Distance of the Tooltip from the Target Object (related to the `placement`) */
@Prop() distance = 10;
/** (optional) Disable the tooltip */
@Prop() disabled? = false;
/** (optional) Tooltip distance from the target element (related to `placement`) */
@Prop() distance? = 10;
/** (optional) How much of the arrow element is "hidden" */
@Prop() arrowOffset?: number = -4;
/** (optional) Set the Tooltip to open per default (will still be closed on closing Events) */
@Prop({ mutable: true, reflect: true }) open = false;
/** (optional) Set custom trigger Event selection */
@Prop() trigger: string = 'hover focus';
/** (optional) Padding between the arrow and the edges of the tooltip */
@Prop() arrowPadding?: number = 8;
/** (optional) Set the tooltip to opened by default (will still be closed on closing events) */
@Prop({ mutable: true, reflect: true }) opened? = false;
/** (optional) Set custom trigger event (hover, focus, click) */
@Prop() trigger?: string = 'hover focus';
/** (optional) Switching the flip option of the tooltip on and off */
@Prop() flip: boolean = true;
@Prop() flip?: boolean = true;
/** (optional) Injected CSS styles */
@Prop() styles?: string;

@State() mouseOverTooltip: boolean = false;

@Event({ eventName: 'scale-before-show' }) tooltipBeforeShow: EventEmitter;
Expand All @@ -74,76 +80,110 @@ export class Tooltip {

private tooltipEl: HTMLElement;
private arrowEl: HTMLElement;
private triggerEl: HTMLElement;

@Watch('open')
@Watch('opened')
handleOpenChange() {
this.open ? this.showTooltip() : this.hideTooltip();
this.opened ? this.showTooltip() : this.hideTooltip();
}

componentDidLoad() {
this.hostEl.addEventListener('blur', this.handleBlur, true);
this.hostEl.addEventListener('click', this.handleClick, true);
this.hostEl.addEventListener('focus', this.handleFocus, true);
connectedCallback() {
statusNote({ source: this.hostElement, tag: 'beta' });

if (this.hostElement.hasAttribute('open')) {
statusNote({
tag: 'deprecated',
message: 'The `open` prop is deprecated in favor of `opened`',
source: this.hostElement,
});
}

const children = Array.from(this.hostElement.children).filter(
(x) => !x.hasAttribute('slot')
);
if (children.length === 0) {
// If not children found to be used as trigger, warn
statusNote({
tag: 'warning',
message: 'An element is required, if using text, wrap it in a `<span>`',
type: 'warn',
source: this.hostElement,
});
return;
}
this.triggerEl = children[0] as HTMLElement;
this.triggerEl.addEventListener('blur', this.handleBlur, true);
this.triggerEl.addEventListener('click', this.handleClick, true);
this.triggerEl.addEventListener('focus', this.handleFocus, true);
this.triggerEl.addEventListener('mouseover', this.handleMouseOver, true);
this.triggerEl.addEventListener('mouseout', this.handleMouseOut, true);
}

disconnectedCallback() {
this.hostEl.removeEventListener('blur', this.handleBlur, true);
this.hostEl.removeEventListener('click', this.handleClick, true);
this.hostEl.removeEventListener('focus', this.handleFocus, true);
this.triggerEl.removeEventListener('blur', this.handleBlur, true);
this.triggerEl.removeEventListener('click', this.handleClick, true);
this.triggerEl.removeEventListener('focus', this.handleFocus, true);
this.triggerEl.removeEventListener('mouseover', this.handleMouseOver, true);
this.triggerEl.removeEventListener('mouseout', this.handleMouseOut, true);
}

@Listen('click', { target: 'document' })
handleOutsideClick(event: MouseEvent) {
if (isClickOutside(event, this.hostEl)) {
if (isClickOutside(event, this.hostElement)) {
this.hideTooltip();
}
}

componentDidUpdate() {
this.update();
if (this.open) {
if (this.opened) {
this.showTooltip();
}
}

update = () => {
if (!this.disabled) {
computePosition(
Array.from(this.hostEl.children).find((x) => !x.hasAttribute('slot')),
this.tooltipEl,
{
placement: this.placement,
middleware: [
offset(this.distance),
...(this.flip ? [flip()] : []),
arrow({ element: this.arrowEl }),
shift({ crossAxis: true }),
],
}
).then(({ x, y, placement, middlewareData }) => {
Object.assign(this.tooltipEl.style, {
left: `${x}px`,
top: `${y}px`,
});

// Accessing the data
const { x: arrowX, y: arrowY } = middlewareData.arrow;

const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]];

Object.assign(this.arrowEl.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: `${this.arrowOffset}px`,
});
});
/**
* @see https://floating-ui.com/docs/tutorial#arrow-middleware
*/
update = async () => {
if (this.disabled || this.triggerEl == null) {
return;
}

// Position tooltip
const { x, y, placement, middlewareData } = await computePosition(
this.triggerEl,
this.tooltipEl,
{
placement: this.placement,
middleware: [
offset(this.distance),
...(this.flip ? [flip()] : []),
arrow({ element: this.arrowEl, padding: this.arrowPadding }),
shift({ crossAxis: true }),
],
}
);
Object.assign(this.tooltipEl.style, {
left: `${x}px`,
top: `${y}px`,
});

// Position arrow
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const [side] = placement.split('-');
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[side];
Object.assign(this.arrowEl.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: `${this.arrowOffset}px`,
});
};

componentDidRender() {
Expand All @@ -152,29 +192,29 @@ export class Tooltip {

@Method()
async showTooltip() {
if (this.open) {
if (this.opened) {
return;
}
const scaleShow = this.tooltipBeforeShow.emit();
if (scaleShow.defaultPrevented) {
this.open = false;
this.opened = false;
return;
}
this.open = true;
this.opened = true;
this.update();
}

@Method()
async hideTooltip() {
if (!this.open) {
if (!this.opened) {
return;
}
const tooltipBeforeHide = this.tooltipBeforeHide.emit();
if (tooltipBeforeHide.defaultPrevented) {
this.open = true;
this.opened = true;
return;
}
this.open = false;
this.opened = false;
this.update();
}

Expand All @@ -186,7 +226,7 @@ export class Tooltip {

handleClick = () => {
if (this.hasTrigger('click')) {
this.open && !this.hasTrigger('focus')
this.opened && !this.hasTrigger('focus')
? this.hideTooltip()
: this.showTooltip();
}
Expand All @@ -199,7 +239,7 @@ export class Tooltip {
};

handleKeyDown = (event: KeyboardEvent) => {
if (this.open && event.key === 'Escape') {
if (this.opened && event.key === 'Escape') {
event.stopPropagation();
this.hideTooltip();
}
Expand Down Expand Up @@ -235,11 +275,7 @@ export class Tooltip {

render() {
return (
<Host
onKeyDown={this.handleKeyDown}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
<Host onKeyDown={this.handleKeyDown}>
{this.styles && <style>{this.styles}</style>}

<span part="trigger" aria-describedby={this.componentId}>
Expand All @@ -250,7 +286,7 @@ export class Tooltip {
<div
part="tooltip"
role="tooltip"
aria-hidden={this.open ? 'false' : 'true'}
aria-hidden={this.opened ? 'false' : 'true'}
ref={(el) => (this.tooltipEl = el)}
id={this.componentId}
tabIndex={0}
Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/utils/status-note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ const tagTypes = {
beta: 'β',
WIP: '🛠 WIP',
deprecated: '😵 Deprecation notice',
warning: 'Warning',
};

const defaultMessages = {
beta:
'This component is currently in beta status. Some things may be refactored. Watch the change log for now.',
WIP: `This component is currently under development and is prone to change. Please wait for its release.\nIt will be available in Storybook once it's finished and documented.`,
deprecated: `This component is deprecated.`,
WIP:
"This component is currently under development and is prone to change. Please wait for its release.\nIt will be available in Storybook once it's finished and documented.",
deprecated: 'This component is deprecated.',
};

interface StatusInterface {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
:placement="placement"
:disabled="disabled"
:distance="distance"
:open="open"
:opened="opened"
:trigger="trigger"
:flip="flip"
>
Expand All @@ -22,7 +22,7 @@ export default {
placement: { type: String },
disabled: { type: Boolean },
distance: { type: Number },
open: { type: Boolean },
opened: { type: Boolean },
trigger: { type: String },
flip: { type: Boolean },
styles: String,
Expand Down
Loading