diff --git a/packages/components/src/components/tooltip/readme.md b/packages/components/src/components/tooltip/readme.md index c2f67fab51..2a170511e3 100644 --- a/packages/components/src/components/tooltip/readme.md +++ b/packages/components/src/components/tooltip/readme.md @@ -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 diff --git a/packages/components/src/components/tooltip/tooltip.css b/packages/components/src/components/tooltip/tooltip.css index d13064a169..cc0341871b 100644 --- a/packages/components/src/components/tooltip/tooltip.css +++ b/packages/components/src/components/tooltip/tooltip.css @@ -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'] { diff --git a/packages/components/src/components/tooltip/tooltip.tsx b/packages/components/src/components/tooltip/tooltip.tsx index b50ca0fb4a..4f1d660ec4 100644 --- a/packages/components/src/components/tooltip/tooltip.tsx +++ b/packages/components/src/components/tooltip/tooltip.tsx @@ -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; @@ -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' @@ -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; @@ -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 ``', + 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() { @@ -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(); } @@ -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(); } @@ -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(); } @@ -235,11 +275,7 @@ export class Tooltip { render() { return ( - + {this.styles && } @@ -250,7 +286,7 @@ export class Tooltip {
(this.tooltipEl = el)} id={this.componentId} tabIndex={0} diff --git a/packages/components/src/utils/status-note.ts b/packages/components/src/utils/status-note.ts index 89a4900a52..0ed7ceae0e 100644 --- a/packages/components/src/utils/status-note.ts +++ b/packages/components/src/utils/status-note.ts @@ -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 { diff --git a/packages/storybook-vue/stories/components/tooltip/ScaleTooltip.vue b/packages/storybook-vue/stories/components/tooltip/ScaleTooltip.vue index 1d18c0490e..ad6fe996a5 100644 --- a/packages/storybook-vue/stories/components/tooltip/ScaleTooltip.vue +++ b/packages/storybook-vue/stories/components/tooltip/ScaleTooltip.vue @@ -5,7 +5,7 @@ :placement="placement" :disabled="disabled" :distance="distance" - :open="open" + :opened="opened" :trigger="trigger" :flip="flip" > @@ -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, diff --git a/packages/storybook-vue/stories/components/tooltip/Tooltip.stories.mdx b/packages/storybook-vue/stories/components/tooltip/Tooltip.stories.mdx index 16db64d0b7..d9ff1cc963 100644 --- a/packages/storybook-vue/stories/components/tooltip/Tooltip.stories.mdx +++ b/packages/storybook-vue/stories/components/tooltip/Tooltip.stories.mdx @@ -39,10 +39,9 @@ import ScaleTooltip from './ScaleTooltip.vue'; control: { type: 'boolean' }, }, distance: { - defaultValue: 5, control: { type: 'text' }, }, - open: { + opened: { defaultValue: false, control: { type: 'boolean' }, }, @@ -68,7 +67,7 @@ export const Template = (args, { argTypes }) => ({ :placement="placement" :disabled="disabled" :distance="distance" - :open="open" + :opened="opened" :trigger="trigger" :flip="flip" :styles="styles" @@ -79,40 +78,27 @@ export const Template = (args, { argTypes }) => ({ }); export const SlotTemplate = (args, { argTypes }) => ({ - components: { ScaleTooltip }, - props: { - ...ScaleTooltip.props, - }, template: ` - - - - +
+ + + I'm a HTML-tooltip + with a link + + Click me + +
`, }); export const FocusTemplate = (args, { argTypes }) => ({ template: `
- - Focus me - + + Focus me +
`, }); @@ -243,12 +229,12 @@ export const FocusTemplate = (args, { argTypes }) => ({ ```html - + I'm a HTML-tooltip with a link - Hover me + Click me ``` diff --git a/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-dark-standard-1-snap.png b/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-dark-standard-1-snap.png index e2e845ba7f..53da16e6d7 100644 Binary files a/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-dark-standard-1-snap.png and b/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-dark-standard-1-snap.png differ diff --git a/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-light-standard-1-snap.png b/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-light-standard-1-snap.png index 5f5cc80d53..5b3615d26a 100644 Binary files a/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-light-standard-1-snap.png and b/packages/visual-tests/src/__image_snapshots__/tooltip-visual-spec-js-tooltip-light-standard-1-snap.png differ