diff --git a/packages/ui-library/src/components/feedback/tooltip/index.css.js b/packages/ui-library/src/components/feedback/tooltip/index.css.js index 3da2b33ec..667cdcd76 100644 --- a/packages/ui-library/src/components/feedback/tooltip/index.css.js +++ b/packages/ui-library/src/components/feedback/tooltip/index.css.js @@ -6,16 +6,16 @@ export const { tokenizedLight: tooltipLight, tokenizedDark: tooltipDark } = rend const { UI } = semanticTokens; return typeSafeNestedCss` - .tooltip { + :host { background-color: ${Tooltip.SurfaceFill}; border-radius: ${Tooltip.ContentCol.BorderRadius}; left: 0; max-width: ${Tooltip.MaxWidth}; min-width: ${Tooltip.MinWidth}; - opacity: 0; + opacity: 1; position: absolute; top: 0; - visibility: hidden; + visibility: visible; width: max-content; .content { @@ -23,44 +23,44 @@ export const { tokenizedLight: tooltipLight, tokenizedDark: tooltipDark } = rend padding: ${Tooltip.ContentCol.Padding}; } - &.elevation { + .elevation { filter: drop-shadow(0 0 1px ${Tooltip.SurfaceFill}); } - &.sm .content { + .sm .content { font-family: ${UI.Caption.SM.fontFamily}, sans-serif; font-size: ${UI.Caption.SM.fontSize}; font-weight: ${UI.Caption.SM.fontWeight}; line-height: ${UI.Caption.SM.lineHeight}; } - &.md .content { + .md .content { font-family: ${UI.Caption.MD.fontFamily}, sans-serif; font-size: ${UI.Caption.MD.fontSize}; font-weight: ${UI.Caption.MD.fontWeight}; line-height: ${UI.Caption.MD.lineHeight}; } - &.lg .content { + .lg .content { font-family: ${UI.Caption.LG.fontFamily}, sans-serif; font-size: ${UI.Caption.LG.fontSize}; font-weight: ${UI.Caption.LG.fontWeight}; line-height: ${UI.Caption.LG.lineHeight}; } - } - .arrow { - align-items: flex-end; - display: flex; - height: 12px; - justify-content: center; - position: absolute; - width: 12px; - z-index: 1; - } - - .arrow path { - fill: ${Tooltip.SurfaceFill}; + .arrow { + align-items: flex-end; + display: flex; + height: 12px; + justify-content: center; + position: absolute; + width: 12px; + z-index: 1; + + > svg > path { + fill: ${Tooltip.SurfaceFill}; + } + } } `; }); diff --git a/packages/ui-library/src/components/feedback/tooltip/index.stories.ts b/packages/ui-library/src/components/feedback/tooltip/index.stories.ts index 25afae74d..db430eca3 100644 --- a/packages/ui-library/src/components/feedback/tooltip/index.stories.ts +++ b/packages/ui-library/src/components/feedback/tooltip/index.stories.ts @@ -1,8 +1,13 @@ -import { html } from 'lit'; +import { LitElement, html } from 'lit'; import { BlrTooltipRenderFunction, BlrTooltipType } from './index'; -import { Themes } from '../../../foundation/_tokens-generated/index.themes'; +import { ThemeType, Themes } from '../../../foundation/_tokens-generated/index.themes'; import { TooltipPlacement } from '../../../globals/constants'; +import { BlrTooltipElementRenderFunction } from './tooltipElement'; +import { setupTooltip } from './setupTooltip'; +import { customElement, property, query } from 'lit/decorators.js'; +import { FormSizesType } from '../../../globals/types'; +import { Placement as PlacementType } from '@floating-ui/dom'; export default { title: 'Design System/Web Components/Feedback/Tooltip', @@ -18,23 +23,36 @@ export default { }, }; -export const BlrTooltipLargeReference = (params: BlrTooltipType) => html`
html`
${BlrTooltipRenderFunction( params, - html`
` + html`
` )}
`; -export const BlrTooltip2SmallReference = (params: BlrTooltipType) => html`
html`
${BlrTooltipRenderFunction(params, html`
`)}
`; -BlrTooltipLargeReference.storyName = 'Tooltip Large Reference'; -BlrTooltip2SmallReference.storyName = 'Tooltip Small Reference'; +export const BlrTooltipVirtualReference = (params: BlrTooltipType) => { + return html` `; +}; + +BlrTooltip.storyName = 'Tooltip Large Reference'; +BlrTooltipSmallReference.storyName = 'Tooltip Small Reference'; +BlrTooltipVirtualReference.storyName = 'Tooltip Virtual Reference'; const args: BlrTooltipType = { theme: 'Light', @@ -46,5 +64,51 @@ const args: BlrTooltipType = { offset: 4, }; -BlrTooltip2SmallReference.args = args; -BlrTooltipLargeReference.args = args; +BlrTooltip.args = args; +BlrTooltipSmallReference.args = args; +BlrTooltipVirtualReference.args = args; + +@customElement('virtual-reference') +export class VirtualReference extends LitElement { + @property() theme: ThemeType = 'Light'; + @property() size: FormSizesType = 'sm'; + @property() text!: string; + @property() placement: PlacementType = 'top'; + @property() hasArrow = true; + @property() elevation = true; + @property() offset = 4; + + @query('blr-tooltip-element') + protected _tooltip!: HTMLElement; + + protected firstUpdated() { + document.addEventListener('mousemove', ({ clientX, clientY }) => { + const virtualReference = { + getBoundingClientRect() { + return { + width: 0, + height: 0, + x: clientX, + y: clientY, + left: clientX, + right: clientX, + top: clientY, + bottom: clientY, + }; + }, + }; + + setupTooltip(virtualReference, this._tooltip, this.placement, 4); + }); + } + + render() { + return html`${BlrTooltipElementRenderFunction({ + theme: this.theme, + text: this.text, + size: this.size, + hasArrow: this.hasArrow, + elevation: this.elevation, + })}`; + } +} diff --git a/packages/ui-library/src/components/feedback/tooltip/index.ts b/packages/ui-library/src/components/feedback/tooltip/index.ts index df1c30237..586c593da 100644 --- a/packages/ui-library/src/components/feedback/tooltip/index.ts +++ b/packages/ui-library/src/components/feedback/tooltip/index.ts @@ -1,108 +1,47 @@ -import { LitElement, TemplateResult, html, nothing } from 'lit'; +import { LitElement, TemplateResult, html } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; -import { tooltipLight, tooltipDark } from './index.css'; -import { computePosition, flip, offset, arrow, Placement, autoUpdate } from '@floating-ui/dom'; +import type { VirtualElement } from '@floating-ui/core'; +import { Placement as PlacementType } from '@floating-ui/dom'; import { FormSizesType } from '../../../globals/types'; import { ThemeType } from '../../../foundation/_tokens-generated/index.themes'; import { genericBlrComponentRenderer } from '../../../utils/typesafe-generic-component-renderer'; -import { classMap } from 'lit/directives/class-map.js'; -import { componentTokens } from '../../../foundation/_tokens-generated/__component-tokens.Light.generated.mjs'; +import { setupTooltip } from './setupTooltip'; +import { BlrTooltipElementRenderFunction } from './tooltipElement'; const TAG_NAME = 'blr-tooltip'; const enterEvents = ['pointerenter', 'focus']; const leaveEvents = ['pointerleave', 'blur', 'keydown', 'click']; -const { - Feedback: { Tooltip }, -} = componentTokens; - @customElement('blr-tooltip') export class BlrTooltip extends LitElement { - @property() theme?: ThemeType = 'Light'; + @property() theme: ThemeType = 'Light'; @property() size: FormSizesType = 'sm'; @property() text!: string; - @property() placement?: Placement = 'top'; - @property() hasArrow? = true; + @property() placement: PlacementType = 'top'; + @property() hasArrow = true; @property() elevation = true; @property() offset = 4; - @query('.tooltip') + @query('blr-tooltip-element') protected _tooltip!: HTMLElement; - @query('.arrow') - protected _arrow!: HTMLElement; - - protected arrowHeight = 4; - - protected _slotTrigger: Element | undefined = undefined; - - connectedCallback() { - super.connectedCallback(); - } + protected _slotReference: Element | undefined = undefined; protected firstUpdated() { - const slot = this?.shadowRoot?.querySelector('slot'); - this._slotTrigger = slot?.assignedElements({ flatten: true })[0]; + const slot = this.shadowRoot?.querySelector('slot'); + this._slotReference = slot?.assignedElements({ flatten: true })[0]; - enterEvents.forEach((event) => this._slotTrigger?.addEventListener(event, this.show)); - leaveEvents.forEach((event) => this._slotTrigger?.addEventListener(event, this.hide)); + enterEvents.forEach((event) => this._slotReference?.addEventListener(event, this.show)); + leaveEvents.forEach((event) => this._slotReference?.addEventListener(event, this.hide)); } - protected setupTooltip = (trigger: Element, tooltip: HTMLElement) => { - autoUpdate(trigger, tooltip, () => { - computePosition(trigger, tooltip, { - placement: this.placement, - middleware: [ - offset((this._arrow && this.arrowHeight) + this.offset), - flip(), - this._arrow && arrow({ element: this._arrow, padding: 8 }), - ], - }).then(({ x, y, middlewareData, placement }) => { - Object.assign(this._tooltip.style, { - left: `${x}px`, - top: `${y}px`, - }); - - if (middlewareData.arrow) { - const { x, y, centerOffset } = middlewareData.arrow; - - const isCentered = centerOffset === 0; - const side = placement.split('-')[0]; - - const staticSide = { - top: 'bottom', - right: 'left', - bottom: 'top', - left: 'right', - }[side]; - - const staticRotation = { - top: 'rotate(0)', - right: 'rotate(90deg)', - bottom: 'rotate(180deg)', - left: 'rotate(-90deg)', - }[side]; - - Object.assign(this._arrow.style, { - top: isCentered && y !== null ? `${y}px` : '', - right: !isCentered && x !== null ? `${x}px` : '', - bottom: !isCentered && y !== null ? `${y}px` : '', - left: isCentered && x !== null ? `${x}px` : '', - transform: staticRotation, - [staticSide]: `-${this.arrowHeight}px`, - }); - } - }); - }); - }; - protected show = () => { - if (!this._slotTrigger) { + if (!this._slotReference) { return; } - this.setupTooltip(this._slotTrigger, this._tooltip); + setupTooltip(this._slotReference, this._tooltip, this.placement, this.offset); this._tooltip.style.visibility = 'visible'; this._tooltip.style.opacity = '1'; @@ -114,32 +53,20 @@ export class BlrTooltip extends LitElement { }; protected render() { - const dynamicStyles = this.theme === 'Light' ? [tooltipLight] : [tooltipDark]; - - const classes = classMap({ - tooltip: true, - [this.size]: this.size, - elevation: this.elevation, - }); - - return html` + return html`
-
-
${this.text}
- ${this.hasArrow - ? html`
- - - -
` - : nothing} -
`; + ${BlrTooltipElementRenderFunction({ + theme: this.theme, + text: this.text, + size: this.size, + hasArrow: this.hasArrow, + elevation: this.elevation, + })} +
`; } } export type BlrTooltipType = Omit; -export const BlrTooltipRenderFunction = (params: BlrTooltipType, children?: TemplateResult<1>) => +export const BlrTooltipRenderFunction = (params: BlrTooltipType, children: TemplateResult<1>) => genericBlrComponentRenderer(TAG_NAME, { ...params }, children); diff --git a/packages/ui-library/src/components/feedback/tooltip/indexReact.ts b/packages/ui-library/src/components/feedback/tooltip/indexReact.ts new file mode 100644 index 000000000..4a7df2c36 --- /dev/null +++ b/packages/ui-library/src/components/feedback/tooltip/indexReact.ts @@ -0,0 +1,10 @@ +import React from 'react'; +import { createComponent } from '@lit-labs/react'; + +import { BlrTooltip } from '.'; + +export const BlrToolTipReact = createComponent({ + tagName: 'blr-tooltip', + elementClass: BlrTooltip, + react: React, +}); diff --git a/packages/ui-library/src/components/feedback/tooltip/setupTooltip.ts b/packages/ui-library/src/components/feedback/tooltip/setupTooltip.ts new file mode 100644 index 000000000..24ca7c270 --- /dev/null +++ b/packages/ui-library/src/components/feedback/tooltip/setupTooltip.ts @@ -0,0 +1,81 @@ +import { + VirtualElement, + autoUpdate, + computePosition, + flip, + offset, + arrow, + Placement as PlacementType, +} from '@floating-ui/dom'; +import { componentTokens } from '../../../foundation/_tokens-generated/__component-tokens.Light.generated.mjs'; + +const { + Feedback: { Tooltip }, +} = componentTokens; + +export const setupTooltip = ( + reference: Element | VirtualElement, + tooltip: HTMLElement, + placement: PlacementType, + offsetValue: number +) => { + const side = placement.split('-')[0]; + + const arrowNode = tooltip.shadowRoot?.querySelector('.arrow'); + + const arrowNodeHeight = arrowNode ? 4 : 0; + const arrowNodePaddingTopBottom = parseFloat(Tooltip.NoseWrapper.PaddingTopBottom); + const arrowNodePaddingLeftRight = parseFloat(Tooltip.NoseWrapper.PaddingLeftRight); + + const arrowNodePadding = + side === 'top' || 'bottom' + ? arrowNodePaddingTopBottom + : side === 'left' || 'right' + ? arrowNodePaddingLeftRight + : undefined; + + autoUpdate(reference, tooltip, () => { + computePosition(reference, tooltip, { + placement, + middleware: [ + offset(arrowNodeHeight + offsetValue), + flip(), + arrowNode && arrow({ element: arrowNode, padding: arrowNodePadding }), + ], + }).then(({ x, y, middlewareData }) => { + Object.assign(tooltip.style, { + left: `${x}px`, + top: `${y}px`, + }); + + if (middlewareData.arrow) { + const { x, y, centerOffset } = middlewareData.arrow; + + const isCentered = centerOffset === 0; + + const staticSide = { + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right', + }[side]; + + const staticRotation = { + top: 'rotate(0)', + right: 'rotate(90deg)', + bottom: 'rotate(180deg)', + left: 'rotate(-90deg)', + }[side]; + + Object.assign(arrowNode.style, { + top: isCentered && y !== null ? `${y}px` : '', + right: !isCentered && x !== null ? `${x}px` : '', + bottom: !isCentered && y !== null ? `${y}px` : '', + left: isCentered && x !== null ? `${x}px` : '', + transform: staticRotation, + [`${staticSide}`]: `-${arrowNodeHeight}px`, + }); + } + }); + }); +}; diff --git a/packages/ui-library/src/components/feedback/tooltip/tooltipElement.ts b/packages/ui-library/src/components/feedback/tooltip/tooltipElement.ts new file mode 100644 index 000000000..1e2105ce0 --- /dev/null +++ b/packages/ui-library/src/components/feedback/tooltip/tooltipElement.ts @@ -0,0 +1,46 @@ +import { LitElement, html, nothing } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { tooltipLight, tooltipDark } from './index.css'; +import { FormSizesType } from '../../../globals/types'; +import { ThemeType } from '../../../foundation/_tokens-generated/index.themes'; +import { genericBlrComponentRenderer } from '../../../utils/typesafe-generic-component-renderer'; +import { classMap } from 'lit/directives/class-map.js'; + +const TAG_NAME = 'blr-tooltip-element'; + +@customElement('blr-tooltip-element') +export class BlrTooltipElement extends LitElement { + @property() theme: ThemeType = 'Light'; + @property() size: FormSizesType = 'sm'; + @property() text!: string; + @property() hasArrow = true; + @property() elevation = true; + + protected render() { + const dynamicStyles = this.theme === 'Light' ? [tooltipLight] : [tooltipDark]; + + const classes = classMap({ + [this.size]: this.size, + elevation: this.elevation, + }); + + return html` +
+
${this.text}
+ ${this.hasArrow + ? html`
+ + + +
` + : nothing} +
`; + } +} + +export type BlrTooltipElementType = Omit; + +export const BlrTooltipElementRenderFunction = (params: BlrTooltipElementType) => + genericBlrComponentRenderer(TAG_NAME, { ...params });