fix: Scrolling to timeslot issue on Safari with inline-embeds (#22547)

* Add playground

* Fix scroll issue on safari
This commit is contained in:
Hariom Balhara
2025-07-23 04:26:08 -03:00
committed by GitHub
parent fa47456d5f
commit 7813d3739f
15 changed files with 657 additions and 19 deletions
+10 -1
View File
@@ -399,7 +399,16 @@
</div>
</div>
<div class="inline-embed-container" id="cal-booking-place-hideEventTypeDetails">
<h3><a href="?only=hideEventTypeDetails">Hide EventType Details Test</a></h3>
<h3><a href="?only=ns:hideEventTypeDetails">Hide EventType Details Test</a></h3>
<div class="place"></div>
</div>
<div class="inline-embed-container" style="width: 500px; overflow-y: scroll;" id="cal-booking-place-windowScrollToTimeslot">
<h3><a href="?only=ns:windowScrollToTimeslot">Scroll to Timeslot Test - Selecting a date would scroll the window to the timeslot</a></h3>
<div class="place"></div>
</div>
<div class="inline-embed-container" style="height: 500px; width: 500px; overflow-y: scroll;" id="cal-booking-place-containerScrollToTimeslot">
<h3><a href="?only=ns:containerScrollToTimeslot">Container Scroll Test - Selecting a date would scroll the element to the timeslot</a></h3>
<div class="place"></div>
</div>
<div class="inline-embed-container" id="cal-booking-place-conflicting-theme">
@@ -288,7 +288,7 @@ if (only === "all" || only === "inline-routing-form") {
]);
}
if (only === "all" || only === "hideEventTypeDetails") {
if (only === "all" || only === "ns:hideEventTypeDetails") {
const identifier = "hideEventTypeDetails";
Cal("init", identifier, {
debug: true,
@@ -693,6 +693,40 @@ if (only === "all" || only === "ns:routingFormWithoutPrerender") {
});
}
if (only === "all" || only === "ns:containerScrollToTimeslot") {
Cal("init", "containerScrollToTimeslot", {
debug: true,
origin,
});
Cal.ns.containerScrollToTimeslot("inline", {
elementOrSelector: "#cal-booking-place-containerScrollToTimeslot .place",
calLink: "free/30min",
config: {
iframeAttrs: {
id: "cal-booking-place-containerScrollToTimeslot-iframe",
},
"flag.coep": "true",
},
});
}
if (only === "all" || only === "ns:windowScrollToTimeslot") {
Cal("init", "windowScrollToTimeslot", {
debug: true,
origin,
});
Cal.ns.windowScrollToTimeslot("inline", {
elementOrSelector: "#cal-booking-place-windowScrollToTimeslot .place",
calLink: "free/30min",
config: {
iframeAttrs: {
id: "cal-booking-place-windowScrollToTimeslot-iframe",
},
"flag.coep": "true",
},
});
}
// Keep it at the bottom as it works on the API defined above for various cases
(function ensureScrolledToCorrectIframe() {
// Reset the hash so that we can scroll to correct iframe
@@ -1,6 +1,6 @@
import { EmbedElement } from "../EmbedElement";
import { getErrorString } from "../lib/utils";
import loaderCss from "../loader.css?inline";
import { getErrorString } from "../utils";
import inlineHtml, { getSkeletonData } from "./inlineHtml";
export class Inline extends EmbedElement {
@@ -1,6 +1,6 @@
import { EmbedElement } from "../EmbedElement";
import { getErrorString } from "../lib/utils";
import loaderCss from "../loader.css";
import { getErrorString } from "../utils";
import modalBoxHtml, { getSkeletonData } from "./ModalBoxHtml";
export class ModalBox extends EmbedElement {
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { generateDataAttributes, isSameBookingLink } from "../utils";
import { generateDataAttributes, isSameBookingLink } from "../lib/utils";
describe("generateDataAttributes", () => {
it("should handle PascalCase property names correctly", () => {
@@ -1,3 +1,4 @@
import { isParamValuePresentInUrlSearchParams } from "../../lib/utils";
import type {
EmbedThemeConfig,
UiConfig,
@@ -6,7 +7,6 @@ import type {
SetStyles,
setNonStylesConfig,
} from "../../types";
import { isParamValuePresentInUrlSearchParams } from "../../utils";
import { runAsap } from "./utils";
export const enum EMBED_IFRAME_STATE {
+24 -6
View File
@@ -10,11 +10,8 @@ import {
} from "./constants";
import type { InterfaceWithParent, interfaceWithParent } from "./embed-iframe";
import css from "./embed.css";
import { SdkActionManager } from "./sdk-action-manager";
import type { EventData, EventDataMap } from "./sdk-action-manager";
import tailwindCss from "./tailwindCss";
import type { UiConfig, EmbedPageType, PrefillAndIframeAttrsConfig, ModalPrerenderOptions } from "./types";
import { getMaxHeightForModal } from "./ui-utils";
import { getScrollableAncestor } from "./lib/domUtils";
import { getScrollByDistanceHandler } from "./lib/eventHandlers/scrollByDistanceEventHandler";
import {
fromEntriesWithDuplicateKeys,
isRouterPath,
@@ -22,7 +19,12 @@ import {
getConfigProp,
isSameBookingLink,
buildConfigWithPrerenderRelatedFields,
} from "./utils";
} from "./lib/utils";
import { SdkActionManager } from "./sdk-action-manager";
import type { EventData, EventDataMap } from "./sdk-action-manager";
import tailwindCss from "./tailwindCss";
import type { UiConfig, EmbedPageType, PrefillAndIframeAttrsConfig, ModalPrerenderOptions } from "./types";
import { getMaxHeightForModal } from "./ui-utils";
// Exporting for consumption by @calcom/embed-core user
export type { EmbedEvent } from "./sdk-action-manager";
@@ -472,6 +474,8 @@ export class Cal {
}
});
this.actionManager.on("__scrollByDistance", getScrollByDistanceHandler(this));
this.actionManager.on("linkReady", () => {
if (this.isPrerendering) {
// Ensure that we don't mark embed as loaded if it's prerendering otherwise prerendered embed could show-up without any user action
@@ -500,6 +504,20 @@ export class Cal {
});
}
scrollByDistance(distanceInPixels: number): void {
if (!this.iframe) {
return;
}
// We compute scrollable ancestor on demand here and not when the iframe is created because because we need to see if the ancestor has scrollable content at that time
const scrollContainer = getScrollableAncestor(this.iframe);
if (!scrollContainer) {
return;
}
const newScrollTop = scrollContainer.scrollTop + distanceInPixels;
scrollContainer.scrollTo({ top: newScrollTop, behavior: "smooth" });
}
private filterParams(params: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(params).filter(([key, value]) => !excludeParam(key, value)));
}
@@ -0,0 +1,463 @@
import { describe, expect, it, beforeEach, vi, beforeAll, afterEach } from "vitest";
import { getScrollableAncestor } from "./domUtils";
function createMockElement(options: {
scrollHeight?: number;
clientHeight?: number;
overflowY?: string;
overflow?: string;
parentElement?: HTMLElement | null;
}): HTMLElement {
const element = document.createElement("div");
// Set up scroll properties
Object.defineProperty(element, "scrollHeight", {
value: options.scrollHeight ?? 100,
writable: true,
});
Object.defineProperty(element, "clientHeight", {
value: options.clientHeight ?? 100,
writable: true,
});
// Set up parent relationship
if (options.parentElement !== undefined) {
Object.defineProperty(element, "parentElement", {
value: options.parentElement,
writable: true,
});
}
return element;
}
function mockGetComputedStyle(styleOverrides: Record<string, Record<string, string>> = {}) {
return vi.fn().mockImplementation((element: HTMLElement) => {
const elementKey = element.tagName + (element.id ? `#${element.id}` : "");
const styles = styleOverrides[elementKey] || {};
return {
getPropertyValue: vi.fn().mockImplementation((property: string) => {
return styles[property] || "visible";
}),
};
});
}
describe("getScrollableAncestor", () => {
let originalGetComputedStyle: typeof window.getComputedStyle;
let originalDocumentElement: Document["documentElement"];
let originalScrollingElement: Document["scrollingElement"];
beforeAll(() => {
originalGetComputedStyle = window.getComputedStyle;
originalDocumentElement = document.documentElement;
originalScrollingElement = document.scrollingElement;
});
beforeEach(() => {
// Reset document.scrollingElement to a default mock
Object.defineProperty(document, "scrollingElement", {
value: document.createElement("html"),
writable: true,
configurable: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
afterAll(() => {
window.getComputedStyle = originalGetComputedStyle;
Object.defineProperty(document, "documentElement", {
value: originalDocumentElement,
writable: true,
configurable: true,
});
Object.defineProperty(document, "scrollingElement", {
value: originalScrollingElement,
writable: true,
configurable: true,
});
});
describe("with scrollable ancestors", () => {
it("should return the first scrollable ancestor with overflow-y: auto", () => {
const scrollableParent = createMockElement({
scrollHeight: 200,
clientHeight: 100,
});
scrollableParent.id = "scrollable-parent";
const targetElement = createMockElement({
parentElement: scrollableParent,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#scrollable-parent": {
"overflow-y": "auto",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(scrollableParent);
});
it("should return the first scrollable ancestor with overflow-y: scroll", () => {
const scrollableParent = createMockElement({
scrollHeight: 200,
clientHeight: 100,
});
scrollableParent.id = "scrollable-parent";
const targetElement = createMockElement({
parentElement: scrollableParent,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#scrollable-parent": {
"overflow-y": "scroll",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(scrollableParent);
});
it("should return the first scrollable ancestor with overflow: auto", () => {
const scrollableParent = createMockElement({
scrollHeight: 200,
clientHeight: 100,
});
scrollableParent.id = "scrollable-parent";
const targetElement = createMockElement({
parentElement: scrollableParent,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#scrollable-parent": {
"overflow-y": "visible",
overflow: "auto",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(scrollableParent);
});
it("should return the first scrollable ancestor with overflow: scroll", () => {
const scrollableParent = createMockElement({
scrollHeight: 200,
clientHeight: 100,
});
scrollableParent.id = "scrollable-parent";
const targetElement = createMockElement({
parentElement: scrollableParent,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#scrollable-parent": {
"overflow-y": "visible",
overflow: "scroll",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(scrollableParent);
});
it("should skip non-scrollable ancestors and find the scrollable one", () => {
const scrollableGrandparent = createMockElement({
scrollHeight: 300,
clientHeight: 150,
});
scrollableGrandparent.id = "scrollable-grandparent";
const nonScrollableParent = createMockElement({
scrollHeight: 100,
clientHeight: 100,
parentElement: scrollableGrandparent,
});
nonScrollableParent.id = "non-scrollable-parent";
const targetElement = createMockElement({
parentElement: nonScrollableParent,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#scrollable-grandparent": {
"overflow-y": "auto",
overflow: "visible",
},
"DIV#non-scrollable-parent": {
"overflow-y": "visible",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(scrollableGrandparent);
});
});
describe("elements with scrollable styles but no scrollable content", () => {
it("should skip elements with overflow: auto but no scrollable content", () => {
const parentWithoutScrollableContent = createMockElement({
scrollHeight: 100,
clientHeight: 100, // Equal heights = no scrollable content
});
parentWithoutScrollableContent.id = "parent-no-content";
const targetElement = createMockElement({
parentElement: parentWithoutScrollableContent,
});
// Set parent's parent to null to reach the end of the chain
Object.defineProperty(parentWithoutScrollableContent, "parentElement", {
value: null,
writable: true,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#parent-no-content": {
"overflow-y": "auto",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(document.scrollingElement);
});
it("should skip elements with scrollHeight < clientHeight", () => {
const parentWithSmallerScrollHeight = createMockElement({
scrollHeight: 80,
clientHeight: 100, // clientHeight > scrollHeight = no scrollable content
});
parentWithSmallerScrollHeight.id = "parent-smaller-scroll";
const targetElement = createMockElement({
parentElement: parentWithSmallerScrollHeight,
});
Object.defineProperty(parentWithSmallerScrollHeight, "parentElement", {
value: null,
writable: true,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#parent-smaller-scroll": {
"overflow-y": "scroll",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(document.scrollingElement);
});
});
describe("fallback to document.scrollingElement", () => {
it("should return document.scrollingElement when no scrollable ancestors found", () => {
const nonScrollableParent = createMockElement({
scrollHeight: 100,
clientHeight: 100,
});
nonScrollableParent.id = "non-scrollable";
const targetElement = createMockElement({
parentElement: nonScrollableParent,
});
Object.defineProperty(nonScrollableParent, "parentElement", {
value: null,
writable: true,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#non-scrollable": {
"overflow-y": "visible",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(document.scrollingElement);
});
it("should return null when document.scrollingElement is null", () => {
Object.defineProperty(document, "scrollingElement", {
value: null,
writable: true,
configurable: true,
});
const nonScrollableParent = createMockElement({
scrollHeight: 100,
clientHeight: 100,
});
const targetElement = createMockElement({
parentElement: nonScrollableParent,
});
Object.defineProperty(nonScrollableParent, "parentElement", {
value: null,
writable: true,
});
window.getComputedStyle = mockGetComputedStyle({
DIV: {
"overflow-y": "visible",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(null);
});
});
describe("edge cases", () => {
it("should handle element with no parent", () => {
const orphanElement = createMockElement({
parentElement: null,
});
const result = getScrollableAncestor(orphanElement);
expect(result).toBe(document.scrollingElement);
});
it("should stop at document.documentElement", () => {
const documentElement = document.createElement("html");
Object.defineProperty(document, "documentElement", {
value: documentElement,
writable: true,
configurable: true,
});
const parentElement = createMockElement({
scrollHeight: 200,
clientHeight: 100,
parentElement: documentElement,
});
parentElement.id = "parent-of-doc-element";
const targetElement = createMockElement({
parentElement: parentElement,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#parent-of-doc-element": {
"overflow-y": "visible",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(document.scrollingElement);
});
it("should handle various overflow values correctly", () => {
const tests = [
{ overflowY: "hidden", overflow: "visible", shouldFind: false },
{ overflowY: "visible", overflow: "hidden", shouldFind: false },
{ overflowY: "auto", overflow: "hidden", shouldFind: true },
{ overflowY: "scroll", overflow: "hidden", shouldFind: true },
{ overflowY: "hidden", overflow: "auto", shouldFind: true },
{ overflowY: "hidden", overflow: "scroll", shouldFind: true },
];
tests.forEach(({ overflowY, overflow, shouldFind }, index) => {
const scrollableParent = createMockElement({
scrollHeight: 200,
clientHeight: 100,
});
scrollableParent.id = `test-parent-${index}`;
const targetElement = createMockElement({
parentElement: scrollableParent,
});
Object.defineProperty(scrollableParent, "parentElement", {
value: null,
writable: true,
});
window.getComputedStyle = mockGetComputedStyle({
[`DIV#test-parent-${index}`]: {
"overflow-y": overflowY,
overflow: overflow,
},
});
const result = getScrollableAncestor(targetElement);
if (shouldFind) {
expect(result).toBe(scrollableParent);
} else {
expect(result).toBe(document.scrollingElement);
}
});
});
});
describe("complex DOM trees", () => {
it("should traverse deep DOM trees correctly", () => {
// Create a deep nesting: target -> parent1 -> parent2 -> parent3 -> scrollableParent
const scrollableParent = createMockElement({
scrollHeight: 400,
clientHeight: 200,
});
scrollableParent.id = "scrollable-root";
const parent3 = createMockElement({
scrollHeight: 100,
clientHeight: 100,
parentElement: scrollableParent,
});
const parent2 = createMockElement({
scrollHeight: 100,
clientHeight: 100,
parentElement: parent3,
});
const parent1 = createMockElement({
scrollHeight: 100,
clientHeight: 100,
parentElement: parent2,
});
const targetElement = createMockElement({
parentElement: parent1,
});
Object.defineProperty(scrollableParent, "parentElement", {
value: null,
writable: true,
});
window.getComputedStyle = mockGetComputedStyle({
"DIV#scrollable-root": {
"overflow-y": "auto",
overflow: "visible",
},
DIV: {
"overflow-y": "visible",
overflow: "visible",
},
});
const result = getScrollableAncestor(targetElement);
expect(result).toBe(scrollableParent);
});
});
});
@@ -0,0 +1,29 @@
export function getScrollableAncestor(element: Element): Element | null {
// Start from parent because we are looking for ancestors of the element.
let currentElement: HTMLElement | null = element.parentElement;
// Walk up the DOM tree to find the first scrollable(across y-axis) ancestor
while (currentElement && currentElement !== document.documentElement) {
const computedStyle = window.getComputedStyle(currentElement);
const overflowY = computedStyle.getPropertyValue("overflow-y");
const overflow = computedStyle.getPropertyValue("overflow");
// Check if element is scrollable
const isScrollable = ["auto", "scroll"].includes(overflowY) || ["auto", "scroll"].includes(overflow);
// Check if element actually has scrollable content
const hasScrollableContent = currentElement.scrollHeight > currentElement.clientHeight;
if (isScrollable && hasScrollableContent) {
return currentElement;
}
currentElement = currentElement.parentElement;
}
if (!document.scrollingElement) {
return null;
}
return document.scrollingElement;
}
@@ -0,0 +1,15 @@
import type { EmbedEvent } from "../../sdk-action-manager";
export const getScrollByDistanceHandler =
(cal: { inlineEl?: Element; scrollByDistance: (distance: number) => void }) =>
(e: EmbedEvent<"__scrollByDistance">) => {
if (!cal.inlineEl) {
// Except inline, others use modalbox which has scroll on iframe itself and thus an attempt to scroll by the child would already be successful
console.warn("scrollBy event received but ignored as it isn't an inline embed");
return;
}
const distanceRelativeToIframe = e.detail.data.distance;
// Note: In case of inline embed, Iframe's height is always up-to-date to ensure that there is no vertical scrollbar in the iframe.
// So, we could just scroll by the distance relative to the iframe and that should bring the content in the iframe into view.
cal.scrollByDistance(distanceRelativeToIframe);
};
@@ -1,4 +1,4 @@
import type { KnownConfig, PrefillAndIframeAttrsConfig } from "./types";
import type { KnownConfig, PrefillAndIframeAttrsConfig } from "../types";
export const getErrorString = ({
errorCode,
@@ -111,6 +111,12 @@ export type EventDataMap = {
iframeWidth: number;
isFirstTime: boolean;
};
__scrollByDistance: {
/**
* Distance in pixels to scroll by.
*/
distance: number;
};
};
export type EventData<T extends keyof EventDataMap> = {
@@ -7,12 +7,12 @@ export default createRule({
name: "no-scroll-into-view-embed",
meta: {
docs: {
description: "Disallow usage of scrollIntoView in embed mode",
description: "Disallow usage of scrollIntoView and scrollIntoViewSmooth in embed mode",
recommended: "error",
},
messages: {
noScrollIntoViewForEmbed:
"Make sure to call scrollIntoView conditionally if it is called without user action. Use useIsEmbed() to detect if embed mode and then don't call it for embed case.",
"Make sure to call scrollIntoView/scrollIntoViewSmooth conditionally if it is called without user action. Use useIsEmbed() to detect if embed mode and then don't call it for embed case.",
},
type: "problem",
schema: [],
@@ -24,13 +24,19 @@ export default createRule({
const { callee } = node;
if (callee.type === "MemberExpression") {
if (callee.property.type === "Identifier" && callee.property.name === "scrollIntoView") {
if (
callee.property.type === "Identifier" &&
(callee.property.name === "scrollIntoView" || callee.property.name === "scrollIntoViewSmooth")
) {
context.report({
node,
messageId: "noScrollIntoViewForEmbed",
});
}
} else if (callee.type === "Identifier" && callee.name === "scrollIntoView") {
} else if (
callee.type === "Identifier" &&
(callee.name === "scrollIntoView" || callee.name === "scrollIntoViewSmooth")
) {
context.report({
node,
messageId: "noScrollIntoViewForEmbed",
+4 -2
View File
@@ -13,6 +13,7 @@ import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
import { scrollIntoViewSmooth } from "@calcom/lib/browser/browser.utils";
import { PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM } from "@calcom/lib/constants";
import { CLOUDFLARE_SITE_ID, CLOUDFLARE_USE_TURNSTILE_IN_BOOKER } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
@@ -154,10 +155,11 @@ const BookerComponent = ({
} = calendars;
const scrolledToTimeslotsOnce = useRef(false);
const scrollToTimeSlots = () => {
if (isMobile && !scrolledToTimeslotsOnce.current) {
if (isMobile && !scrolledToTimeslotsOnce.current && timeslotsRef.current) {
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- We are allowing it here because scrollToTimeSlots is called on explicit user action where it makes sense to scroll, remember that the goal is to not do auto-scroll on embed load because that ends up scrolling the embedding webpage too
timeslotsRef.current?.scrollIntoView({ behavior: "smooth" });
scrollIntoViewSmooth(timeslotsRef.current, isEmbed);
scrolledToTimeslotsOnce.current = true;
}
};
+56
View File
@@ -1,3 +1,5 @@
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
type BrowserInfo = {
url: string;
path: string;
@@ -20,3 +22,57 @@ export const getBrowserInfo = (): Partial<BrowserInfo> => {
origin: window.document.location?.origin,
};
};
export const isSafariBrowser = (): boolean => {
if (typeof window === "undefined") return false;
const ua = navigator.userAgent.toLowerCase();
return ua.includes("safari") && !ua.includes("chrome");
};
/**
* Asks parent to scroll to an element inside iframe.
* This is a workaround for Safari iframe scrolling behavior and so isn't recommended to be used by external consumers.
*/
const askParentToScrollToElement = (element: HTMLElement): void => {
const elementRect = element.getBoundingClientRect();
const elementTop = elementRect.top;
sdkActionManager?.fire("__scrollByDistance", {
distance: elementTop,
});
};
const afterNthPaintCycle = (n: number, callback: () => void): void => {
requestAnimationFrame(() => {
if (n === 1) {
callback();
return;
}
requestAnimationFrame(() => afterNthPaintCycle(n - 1, callback));
});
};
/**
* Cross-browser compatible scrollIntoView with Safari iframe support
*/
export const scrollIntoViewSmooth = (element: HTMLElement, isEmbed = false): void => {
const currentPosition = element.getBoundingClientRect().top;
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed
element.scrollIntoView({ behavior: "smooth" });
if (!isEmbed) {
return;
}
const mightNeedSafariWorkaround = isSafariBrowser();
// First paint cycle will actually start scrolling the element.
// So, we need to wait for the second paint cycle to guarantee that the element is scrolled some amount.
afterNthPaintCycle(2, () => {
const newPosition = element.getBoundingClientRect().top;
const didScroll = currentPosition !== newPosition;
const scrollWorkaroundNeeded = mightNeedSafariWorkaround && !didScroll;
if (!scrollWorkaroundNeeded) {
return;
}
askParentToScrollToElement(element);
});
};