Skip to content
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
4 changes: 3 additions & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ fileignoreconfig:
- filename: README.md
checksum: 568289bbe7c088967493db246dbf29e465382648ac574c1b1236be57d5662a38
- filename: CHANGELOG.md
checksum: 09ed2613ba45ee13b6dbb4fc178911e93674d4e5c40af026d66266ea172374a4
checksum: ed794e2f5c5884f74af12e5f5bfbb117c08ba454104f929df3deb7627407317a
- filename: src/visualBuilder/components/__test__/fieldToolbar.test.tsx
checksum: 3badd6a142456b6a361569e6fc546349a38ac6b366bef7fd5255d1e93220444e
- filename: src/visualBuilder/components/Collab/ThreadPopup/__test__/CommentTextArea.test.tsx
checksum: d0ef271ee5381d9feab06bda6e7e89bd0a882fee87495627bd811c1f0a5459c7
- filename: package-lock.json
checksum: fd06363871d0ee16ebfb5d9d0cc479e0922a615bb76584b80bb6933ee6c3e237
version: "1.0"
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
# Changelog

## [v3.3.0](https://github.com/contentstack/live-preview-sdk/compare/v3.2.5...v3.3.0)

> 24 July 2025

### Fixes

- Fix: HoverToolbar to not render when focussed (Ayush Dubey - [#461](https://github.com/contentstack/live-preview-sdk/pull/461))

### General Changes

- Release 24 July to stage_v3 (Sairaj - [#473](https://github.com/contentstack/live-preview-sdk/pull/473))
- HoverToolbar: Requested Changes (Ayush Dubey - [#464](https://github.com/contentstack/live-preview-sdk/pull/464))
- [Feature] HoverToolbar (Ayush Dubey - [#455](https://github.com/contentstack/live-preview-sdk/pull/455))

## [v3.2.5](https://github.com/contentstack/live-preview-sdk/compare/v3.2.4...v3.2.5)

> 9 July 2025
> 10 July 2025

### New Features

- feat: v3.2.5 lp sdk (Karan Gandhi - [#454](https://github.com/contentstack/live-preview-sdk/pull/454))
- feat: v3.2.5 (Karan Gandhi - [#452](https://github.com/contentstack/live-preview-sdk/pull/452))

### Fixes

Expand All @@ -24,9 +43,11 @@
- fix: psuedo-editable height collapse (Faraaz Biyabani - [f28d629](https://github.com/contentstack/live-preview-sdk/commit/f28d629d362d5820b8583f748b42bd98d464c180))
- fix: changed DOM events (csAyushDubey - [8e433b4](https://github.com/contentstack/live-preview-sdk/commit/8e433b41328acefd969ba157d25cf6f6ad5cc351))
- fix: test fix (csAyushDubey - [af6acf5](https://github.com/contentstack/live-preview-sdk/commit/af6acf5eba9236ba3fb13bb32da8fdade9063d51))
- fix: talisman update (Karan Gandhi - [cf73f0b](https://github.com/contentstack/live-preview-sdk/commit/cf73f0b267e3c42e2fce13579ca014d5edae1a57))

### Chores And Housekeeping

- chore: changelog update (Karan Gandhi - [f3a512e](https://github.com/contentstack/live-preview-sdk/commit/f3a512e3b72c9a956b2d1580af34d6c4c7e94ecc))
- chore: update README.md to reference ContentstackLivePreview version 3.2.5 (hiteshshetty-dev - [e063d6e](https://github.com/contentstack/live-preview-sdk/commit/e063d6ef8fd95faaef612981f4586b4db66f9e4d))

## [v3.2.4](https://github.com/contentstack/live-preview-sdk/compare/v3.2.3...v3.2.4)
Expand Down
27 changes: 25 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/live-preview-utils",
"version": "3.2.5",
"version": "3.3.0",
"description": "Contentstack provides the Live Preview SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane.",
"type": "module",
"types": "dist/legacy/index.d.ts",
Expand Down Expand Up @@ -86,6 +86,7 @@
"url": "https://github.com/contentstack/live-preview-sdk.git"
},
"dependencies": {
"@floating-ui/dom": "^1.7.2",
"@preact/compat": "17.1.2",
"@preact/signals": "1.2.2",
"classnames": "^2.5.1",
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function sleep(waitTimeInMs = 100): Promise<void> {
export const waitForHoverOutline = async () => {
await waitFor(() => {
const hoverOutline = document.querySelector(
"[data-testid='visual-builder__hover-outline']"
"[data-testid='visual-builder__hover-outline'][style]"
);
expect(hoverOutline).not.toBeNull();
});
Expand Down
184 changes: 184 additions & 0 deletions src/visualBuilder/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { h, cloneElement } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import {
computePosition,
flip,
shift,
offset,
arrow
} from '@floating-ui/dom';
import { visualBuilderStyles } from '../visualBuilder.style';
import classNames from 'classnames';
import { ContentTypeIcon } from './icons';
import { FieldTypeIconsMap } from '../generators/generateCustomCursor';
interface TooltipProps {
children: JSX.Element;
content: JSX.Element;
placement?: 'top-start' | 'bottom-start' | 'left-start' | 'right-start';
}

/**
* A lightweight, reusable tooltip component for Preact powered by Floating UI.
*
* @param {object} props - The component props.
* @param {preact.ComponentChildren} props.children - The single child element that triggers the tooltip.
* @param {string | preact.VNode} props.content - The content to display inside the tooltip.
* @param {'top'|'bottom'|'left'|'right'} [props.placement='top'] - The desired placement of the tooltip.
*/
const Tooltip = ({ children, content, placement = 'top-start' }: TooltipProps) => {
const [isVisible, setIsVisible] = useState(false);
// Create refs for the trigger and the floating tooltip elements
const triggerRef = useRef(null);
const tooltipRef = useRef(null);
const arrowRef = useRef(null);

const showTooltip = () => setIsVisible(true);
const hideTooltip = () => setIsVisible(false);

// This effect calculates the tooltip's position whenever it becomes visible
// or if its content or placement changes.
useEffect(() => {
if (!isVisible || !triggerRef.current || !tooltipRef.current) {
return;
}

const trigger = triggerRef.current as HTMLElement;
const tooltip = tooltipRef.current as HTMLElement;

computePosition(trigger, tooltip, {
placement,
// Middleware runs in order to modify the position
middleware: [
offset(8), // Add 8px of space between the trigger and tooltip
flip(), // Flip to the opposite side if it overflows
shift({ padding: 5 }), // Shift to keep it in view
...(arrowRef.current ? [arrow({ element: arrowRef.current as HTMLElement })] : []), // Handle arrow positioning
],
}).then(({ x, y, placement, middlewareData }) => {
// Apply the calculated coordinates to the tooltip element
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`,
});

// Position the arrow element
if (middlewareData.arrow && arrowRef.current) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const side = placement.split('-')[0];
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[side] as string;

const arrowElement = arrowRef.current as HTMLElement;

// Reset all positioning properties
Object.assign(arrowElement.style, {
left: '',
top: '',
right: '',
bottom: '',
});

// For placements like top-start, bottom-start, etc., we want the arrow
// to be centered on the tooltip rather than pointing at the trigger center
if (placement.includes('-start') || placement.includes('-end')) {
const tooltipRect = tooltip.getBoundingClientRect();

if (side === 'top' || side === 'bottom') {
// For top/bottom placements, center the arrow horizontally
arrowElement.style.left = `${14}px`; // 4px = half arrow width
if (arrowY != null) {
arrowElement.style.top = `${arrowY}px`;
}
} else {
// For left/right placements, center the arrow vertically
arrowElement.style.top = `${tooltipRect.height / 2 - 4}px`; // 4px = half arrow height
if (arrowX != null) {
arrowElement.style.left = `${arrowX}px`;
}
}
} else {
// For regular placements (top, bottom, left, right), use floating-ui's positioning
if (arrowX != null) {
arrowElement.style.left = `${arrowX}px`;
}
if (arrowY != null) {
arrowElement.style.top = `${arrowY}px`;
}
}

// Position arrow to overlap the tooltip's border
(arrowElement.style as any)[staticSide] = '-4px';
}
});

}, [isVisible, placement, content]);

// We need to clone the child element to attach our ref and event listeners.
// This ensures we don't wrap the child in an extra <div>.
const triggerWithListeners = cloneElement(children, {
ref: triggerRef,
onMouseEnter: showTooltip,
onMouseLeave: hideTooltip,
onFocus: showTooltip,
onBlur: hideTooltip,
'aria-describedby': 'lightweight-tooltip' // for accessibility
});

return (
<>
{triggerWithListeners}
{isVisible && (
<div
ref={tooltipRef}
role="tooltip"
id="lightweight-tooltip"
className={classNames("tooltip-container", visualBuilderStyles()["tooltip-container"])}
>
{content}
<div ref={arrowRef} className={classNames("tooltip-arrow", visualBuilderStyles()["tooltip-arrow"])}></div>
</div>
)}
</>
);
};

function ToolbarTooltipContent({contentTypeName, referenceFieldName}: {contentTypeName: string, referenceFieldName: string}) {
return (
<div className={classNames("toolbar-tooltip-content", visualBuilderStyles()["toolbar-tooltip-content"])}>
{
contentTypeName && (
<div className={classNames("toolbar-tooltip-content-item", visualBuilderStyles()["toolbar-tooltip-content-item"])}>
<ContentTypeIcon />
<p>{contentTypeName}</p>
</div>
)
}
{
referenceFieldName && (
<div className={classNames("toolbar-tooltip-content-item", visualBuilderStyles()["toolbar-tooltip-content-item"])}>
<div dangerouslySetInnerHTML={{__html: FieldTypeIconsMap.reference}} className={classNames("visual-builder__field-icon", visualBuilderStyles()["visual-builder__field-icon"])}/>
<p>{referenceFieldName}</p>
</div>
)
}
</div>
)
}

export function ToolbarTooltip({children, data, disabled = false}: {children: JSX.Element, data: {contentTypeName: string, referenceFieldName: string}, disabled?: boolean}) {
if (disabled) {
return children;
}
const { contentTypeName, referenceFieldName } = data;
return (
<Tooltip content={<ToolbarTooltipContent contentTypeName={contentTypeName} referenceFieldName={referenceFieldName} />}>
{children}
</Tooltip>
)
}

export default Tooltip;
Loading
Loading