Embed Improvements: Add auto-scroll support back, reduce CLS, remove loader for Routing Form embed (#4819)

* Add back auto-scroll fix with proper check of visibilith

* Do improvement in CLS and dont show "Loading" text

* Improve experience with routing forms embed

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Hariom Balhara
2022-10-14 10:10:54 +00:00
committed by GitHub
co-authored by Alex van Andel
parent 88cad2e15f
commit 8583d9b2e6
7 changed files with 63 additions and 22 deletions
+9 -2
View File
@@ -35,6 +35,12 @@ function toRunBeforeReactOnClient() {
const namespace = url.searchParams.get("embed");
return namespace;
};
window.isPageOptimizedForEmbed = () => {
// Those pages are considered optimized, which know at backend that they are rendering for embed.
// Such pages can be shown straightaway without a loader for a better embed experience
return location.pathname.includes("forms/");
};
}
class MyDocument extends Document<Props> {
@@ -76,7 +82,6 @@ class MyDocument extends Document<Props> {
/>
</Head>
{/* Keep the embed hidden till parent initializes and gives it the appropriate styles */}
<body className="dark:bg-darkgray-50 desktop-transparent bg-gray-100">
<Main />
<NextScript />
@@ -86,7 +91,9 @@ class MyDocument extends Document<Props> {
dangerouslySetInnerHTML={{
__html: `
if (isEmbed()) {
document.body.style.display="none";
if(!isPageOptimizedForEmbed()) {
document.body.style.display="none";
}
document.body.style.background="transparent";
}`,
}}
@@ -168,8 +168,11 @@ expect.extend({
initialValuesSet: window.initialValuesSet,
};
});
const isOptimizedPage = u.pathname.includes("forms/");
expect(initialValuesSet).toBe(true);
expect(displayBefore).toBe("none");
if (!isOptimizedPage) {
expect(displayBefore).toBe("none");
}
expect(backgroundBefore).toBe("transparent");
const { display: displayAfter, background: backgroundAfter } = await iframe.evaluate(() => {
@@ -19,6 +19,7 @@ declare global {
resetEmbedStatus: () => void;
getEmbedNamespace: () => string | null;
getEmbedTheme: () => "dark" | "light" | null;
isPageOptimizedForEmbed: (calLink: string) => boolean;
}
}
@@ -221,7 +222,8 @@ export const useIsEmbed = () => {
// We can't simply return isEmbed() from this method.
// isEmbed() returns different values on server and browser, which messes up the hydration.
// TODO: We can avoid using document.URL and instead use Router.
const [isEmbed, setIsEmbed] = useState<boolean | null>(null);
const router = useRouter();
const [isEmbed, setIsEmbed] = useState<boolean | null>(typeof router.query.embed === "string");
useEffect(() => {
const namespace = getNamespace();
const _isValidNamespace = isValidNamespace(namespace);
@@ -283,7 +285,11 @@ export const methods = {
return;
}
// No UI change should happen in sight. Let the parent height adjust and in next cycle show it.
requestAnimationFrame(unhideBody);
// HACK: React components take time to commit their updates of isEmbed=true to DOM, so add this random 500ms delay which seems to work
// TODO: Optimize page by page and then remove this unnecessary delay
setTimeout(() => {
unhideBody();
}, 500);
sdkActionManager?.fire("linkReady", {});
});
},
+17 -7
View File
@@ -20,6 +20,7 @@ type Namespace = string;
type Config = {
origin: string;
debug?: boolean;
uiDebug?: boolean;
};
const globalCal = (window as CalWindow).Cal;
@@ -184,6 +185,9 @@ export class Cal {
if (config.debug) {
urlInstance.searchParams.set("debug", "" + config.debug);
}
if (config.uiDebug) {
iframe.style.border = "1px solid green";
}
// Merge searchParams from config onto the URL which might have query params already
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -199,10 +203,11 @@ export class Cal {
if (typeof namespaceOrConfig !== "string") {
config = (namespaceOrConfig || {}) as Config;
}
if (config?.origin) {
this.__config.origin = config.origin;
const { origin, ...restConfig } = config;
if (origin) {
this.__config.origin = origin;
}
this.__config.debug = config.debug;
this.__config = { ...this.__config, ...restConfig };
}
getConfig() {
@@ -242,8 +247,8 @@ export class Cal {
},
},
});
const isCalPageOptimized = calLink.includes("forms/");
config = config || {};
// Keeping auto-scroll disabled for two reasons:
// - If user scrolls the content to an appropriate position, it again resets it to default position which might not be for the liking of the user
// - Sometimes, the position can be wrong(e.g. if there is a fixed position header on top coming above the iframe content).
@@ -263,7 +268,9 @@ export class Cal {
throw new Error("Element not found");
}
const template = document.createElement("template");
template.innerHTML = `<cal-inline style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap;width:100%"></cal-inline>`;
template.innerHTML = `<cal-inline ${
isCalPageOptimized ? 'loading="done"' : ""
} style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap;width:100%"></cal-inline>`;
this.inlineEl = template.content.children[0];
(this.inlineEl as unknown as any).__CalAutoScroll = config.__autoScroll;
this.inlineEl.appendChild(iframe);
@@ -483,8 +490,11 @@ export class Cal {
});
this.actionManager.on("__routeChanged", () => {
if (this.inlineEl && (this.inlineEl as unknown as any).__CalAutoScroll) {
this.inlineEl.scrollIntoView();
const { top, height } = this.inlineEl.getBoundingClientRect();
// Try to readjust and scroll into view if more than 25% is hidden.
// Otherwise we assume that user might have positioned the content appropriately already
if (top < 0 && Math.abs(top / height) >= 0.25) {
this.inlineEl.scrollIntoView({ behavior: "smooth" });
}
});
+11
View File
@@ -4,6 +4,17 @@ Embed Cal Link as a React Component
To know how to use it, follow the steps at <https://developer.cal.com/embed/install-with-react>
## Development
Following command starts a hot reloading server
`yarn dev`
If you are working with embed on website, don't forget to do `yarn build` after every change.
## Running Tests
Runs tests and updates the snapshots. Right now we don't care about snapshots
`yarn embed-tests-quick --update-snapshots`
TODO
- Playwright tests.
+11 -8
View File
@@ -6,12 +6,16 @@ import useEmbed from "./useEmbed";
type CalProps = {
calOrigin?: string;
calLink: string;
initConfig?: {
debug?: boolean;
uiDebug?: boolean;
};
config?: any;
embedJsUrl?: string;
} & React.HTMLAttributes<HTMLDivElement>;
const Cal = function Cal(props: CalProps) {
const { calLink, calOrigin, config, embedJsUrl, ...restProps } = props;
const { calLink, calOrigin, config, initConfig = {}, embedJsUrl, ...restProps } = props;
if (!calLink) {
throw new Error("calLink is required");
}
@@ -24,20 +28,19 @@ const Cal = function Cal(props: CalProps) {
}
initializedRef.current = true;
const element = ref.current;
let initConfig = {};
if (calOrigin) {
(initConfig as any).origin = calOrigin;
}
Cal("init", initConfig);
Cal("init", {
...initConfig,
origin: calOrigin,
});
Cal("inline", {
elementOrSelector: element,
calLink,
config,
});
}, [Cal, calLink, config, calOrigin]);
}, [Cal, calLink, config, calOrigin, initConfig]);
if (!Cal) {
return <div {...restProps}>Loading {calLink} </div>;
return null;
}
return <div ref={ref} {...restProps} />;
+3 -2
View File
@@ -45,9 +45,10 @@ function App() {
notes: "Test Meeting",
guests: ["janedoe@gmail.com"],
theme: "dark",
}}></Cal>
}}
/>
<button data-cal-link="pro">Popup</button>
</>
);
}
ReactDom.render(<App></App>, document.getElementById("root"));
ReactDom.render(<App />, document.getElementById("root"));