Skip to content

Commit 95fb850

Browse files
authored
Merge pull request #474 from contentstack/stage_v3
v3.3.0
2 parents a3d3294 + 71913fb commit 95fb850

20 files changed

Lines changed: 1099 additions & 380 deletions

.talismanrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ fileignoreconfig:
55
- filename: README.md
66
checksum: 568289bbe7c088967493db246dbf29e465382648ac574c1b1236be57d5662a38
77
- filename: CHANGELOG.md
8-
checksum: 09ed2613ba45ee13b6dbb4fc178911e93674d4e5c40af026d66266ea172374a4
8+
checksum: ed794e2f5c5884f74af12e5f5bfbb117c08ba454104f929df3deb7627407317a
99
- filename: src/visualBuilder/components/__test__/fieldToolbar.test.tsx
1010
checksum: 3badd6a142456b6a361569e6fc546349a38ac6b366bef7fd5255d1e93220444e
1111
- filename: src/visualBuilder/components/Collab/ThreadPopup/__test__/CommentTextArea.test.tsx
1212
checksum: d0ef271ee5381d9feab06bda6e7e89bd0a882fee87495627bd811c1f0a5459c7
13+
- filename: package-lock.json
14+
checksum: fd06363871d0ee16ebfb5d9d0cc479e0922a615bb76584b80bb6933ee6c3e237
1315
version: "1.0"

CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
11
# Changelog
22

3+
## [v3.3.0](https://github.com/contentstack/live-preview-sdk/compare/v3.2.5...v3.3.0)
4+
5+
> 24 July 2025
6+
7+
### Fixes
8+
9+
- Fix: HoverToolbar to not render when focussed (Ayush Dubey - [#461](https://github.com/contentstack/live-preview-sdk/pull/461))
10+
11+
### General Changes
12+
13+
- Release 24 July to stage_v3 (Sairaj - [#473](https://github.com/contentstack/live-preview-sdk/pull/473))
14+
- HoverToolbar: Requested Changes (Ayush Dubey - [#464](https://github.com/contentstack/live-preview-sdk/pull/464))
15+
- [Feature] HoverToolbar (Ayush Dubey - [#455](https://github.com/contentstack/live-preview-sdk/pull/455))
16+
317
## [v3.2.5](https://github.com/contentstack/live-preview-sdk/compare/v3.2.4...v3.2.5)
418

5-
> 9 July 2025
19+
> 10 July 2025
20+
21+
### New Features
22+
23+
- feat: v3.2.5 lp sdk (Karan Gandhi - [#454](https://github.com/contentstack/live-preview-sdk/pull/454))
24+
- feat: v3.2.5 (Karan Gandhi - [#452](https://github.com/contentstack/live-preview-sdk/pull/452))
625

726
### Fixes
827

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

2848
### Chores And Housekeeping
2949

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

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

package-lock.json

Lines changed: 25 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/live-preview-utils",
3-
"version": "3.2.5",
3+
"version": "3.3.0",
44
"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.",
55
"type": "module",
66
"types": "dist/legacy/index.d.ts",
@@ -86,6 +86,7 @@
8686
"url": "https://github.com/contentstack/live-preview-sdk.git"
8787
},
8888
"dependencies": {
89+
"@floating-ui/dom": "^1.7.2",
8990
"@preact/compat": "17.1.2",
9091
"@preact/signals": "1.2.2",
9192
"classnames": "^2.5.1",

src/__test__/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export async function sleep(waitTimeInMs = 100): Promise<void> {
3939
export const waitForHoverOutline = async () => {
4040
await waitFor(() => {
4141
const hoverOutline = document.querySelector(
42-
"[data-testid='visual-builder__hover-outline']"
42+
"[data-testid='visual-builder__hover-outline'][style]"
4343
);
4444
expect(hoverOutline).not.toBeNull();
4545
});
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { h, cloneElement } from 'preact';
2+
import { useState, useEffect, useRef } from 'preact/hooks';
3+
import {
4+
computePosition,
5+
flip,
6+
shift,
7+
offset,
8+
arrow
9+
} from '@floating-ui/dom';
10+
import { visualBuilderStyles } from '../visualBuilder.style';
11+
import classNames from 'classnames';
12+
import { ContentTypeIcon } from './icons';
13+
import { FieldTypeIconsMap } from '../generators/generateCustomCursor';
14+
interface TooltipProps {
15+
children: JSX.Element;
16+
content: JSX.Element;
17+
placement?: 'top-start' | 'bottom-start' | 'left-start' | 'right-start';
18+
}
19+
20+
/**
21+
* A lightweight, reusable tooltip component for Preact powered by Floating UI.
22+
*
23+
* @param {object} props - The component props.
24+
* @param {preact.ComponentChildren} props.children - The single child element that triggers the tooltip.
25+
* @param {string | preact.VNode} props.content - The content to display inside the tooltip.
26+
* @param {'top'|'bottom'|'left'|'right'} [props.placement='top'] - The desired placement of the tooltip.
27+
*/
28+
const Tooltip = ({ children, content, placement = 'top-start' }: TooltipProps) => {
29+
const [isVisible, setIsVisible] = useState(false);
30+
// Create refs for the trigger and the floating tooltip elements
31+
const triggerRef = useRef(null);
32+
const tooltipRef = useRef(null);
33+
const arrowRef = useRef(null);
34+
35+
const showTooltip = () => setIsVisible(true);
36+
const hideTooltip = () => setIsVisible(false);
37+
38+
// This effect calculates the tooltip's position whenever it becomes visible
39+
// or if its content or placement changes.
40+
useEffect(() => {
41+
if (!isVisible || !triggerRef.current || !tooltipRef.current) {
42+
return;
43+
}
44+
45+
const trigger = triggerRef.current as HTMLElement;
46+
const tooltip = tooltipRef.current as HTMLElement;
47+
48+
computePosition(trigger, tooltip, {
49+
placement,
50+
// Middleware runs in order to modify the position
51+
middleware: [
52+
offset(8), // Add 8px of space between the trigger and tooltip
53+
flip(), // Flip to the opposite side if it overflows
54+
shift({ padding: 5 }), // Shift to keep it in view
55+
...(arrowRef.current ? [arrow({ element: arrowRef.current as HTMLElement })] : []), // Handle arrow positioning
56+
],
57+
}).then(({ x, y, placement, middlewareData }) => {
58+
// Apply the calculated coordinates to the tooltip element
59+
Object.assign(tooltip.style, {
60+
left: `${x}px`,
61+
top: `${y}px`,
62+
});
63+
64+
// Position the arrow element
65+
if (middlewareData.arrow && arrowRef.current) {
66+
const { x: arrowX, y: arrowY } = middlewareData.arrow;
67+
const side = placement.split('-')[0];
68+
const staticSide = {
69+
top: 'bottom',
70+
right: 'left',
71+
bottom: 'top',
72+
left: 'right',
73+
}[side] as string;
74+
75+
const arrowElement = arrowRef.current as HTMLElement;
76+
77+
// Reset all positioning properties
78+
Object.assign(arrowElement.style, {
79+
left: '',
80+
top: '',
81+
right: '',
82+
bottom: '',
83+
});
84+
85+
// For placements like top-start, bottom-start, etc., we want the arrow
86+
// to be centered on the tooltip rather than pointing at the trigger center
87+
if (placement.includes('-start') || placement.includes('-end')) {
88+
const tooltipRect = tooltip.getBoundingClientRect();
89+
90+
if (side === 'top' || side === 'bottom') {
91+
// For top/bottom placements, center the arrow horizontally
92+
arrowElement.style.left = `${14}px`; // 4px = half arrow width
93+
if (arrowY != null) {
94+
arrowElement.style.top = `${arrowY}px`;
95+
}
96+
} else {
97+
// For left/right placements, center the arrow vertically
98+
arrowElement.style.top = `${tooltipRect.height / 2 - 4}px`; // 4px = half arrow height
99+
if (arrowX != null) {
100+
arrowElement.style.left = `${arrowX}px`;
101+
}
102+
}
103+
} else {
104+
// For regular placements (top, bottom, left, right), use floating-ui's positioning
105+
if (arrowX != null) {
106+
arrowElement.style.left = `${arrowX}px`;
107+
}
108+
if (arrowY != null) {
109+
arrowElement.style.top = `${arrowY}px`;
110+
}
111+
}
112+
113+
// Position arrow to overlap the tooltip's border
114+
(arrowElement.style as any)[staticSide] = '-4px';
115+
}
116+
});
117+
118+
}, [isVisible, placement, content]);
119+
120+
// We need to clone the child element to attach our ref and event listeners.
121+
// This ensures we don't wrap the child in an extra <div>.
122+
const triggerWithListeners = cloneElement(children, {
123+
ref: triggerRef,
124+
onMouseEnter: showTooltip,
125+
onMouseLeave: hideTooltip,
126+
onFocus: showTooltip,
127+
onBlur: hideTooltip,
128+
'aria-describedby': 'lightweight-tooltip' // for accessibility
129+
});
130+
131+
return (
132+
<>
133+
{triggerWithListeners}
134+
{isVisible && (
135+
<div
136+
ref={tooltipRef}
137+
role="tooltip"
138+
id="lightweight-tooltip"
139+
className={classNames("tooltip-container", visualBuilderStyles()["tooltip-container"])}
140+
>
141+
{content}
142+
<div ref={arrowRef} className={classNames("tooltip-arrow", visualBuilderStyles()["tooltip-arrow"])}></div>
143+
</div>
144+
)}
145+
</>
146+
);
147+
};
148+
149+
function ToolbarTooltipContent({contentTypeName, referenceFieldName}: {contentTypeName: string, referenceFieldName: string}) {
150+
return (
151+
<div className={classNames("toolbar-tooltip-content", visualBuilderStyles()["toolbar-tooltip-content"])}>
152+
{
153+
contentTypeName && (
154+
<div className={classNames("toolbar-tooltip-content-item", visualBuilderStyles()["toolbar-tooltip-content-item"])}>
155+
<ContentTypeIcon />
156+
<p>{contentTypeName}</p>
157+
</div>
158+
)
159+
}
160+
{
161+
referenceFieldName && (
162+
<div className={classNames("toolbar-tooltip-content-item", visualBuilderStyles()["toolbar-tooltip-content-item"])}>
163+
<div dangerouslySetInnerHTML={{__html: FieldTypeIconsMap.reference}} className={classNames("visual-builder__field-icon", visualBuilderStyles()["visual-builder__field-icon"])}/>
164+
<p>{referenceFieldName}</p>
165+
</div>
166+
)
167+
}
168+
</div>
169+
)
170+
}
171+
172+
export function ToolbarTooltip({children, data, disabled = false}: {children: JSX.Element, data: {contentTypeName: string, referenceFieldName: string}, disabled?: boolean}) {
173+
if (disabled) {
174+
return children;
175+
}
176+
const { contentTypeName, referenceFieldName } = data;
177+
return (
178+
<Tooltip content={<ToolbarTooltipContent contentTypeName={contentTypeName} referenceFieldName={referenceFieldName} />}>
179+
{children}
180+
</Tooltip>
181+
)
182+
}
183+
184+
export default Tooltip;

0 commit comments

Comments
 (0)