calcom / cal.com

Scheduling infrastructure for absolutely everyone.
https://cal.com
Other
31.93k stars 7.81k forks source link

[CAL-4095] Embedded Calendar View Flickering between Light and Dark mode (bug) #15922

Closed nzayatz14 closed 1 month ago

nzayatz14 commented 2 months ago

Issue Summary

Hi there! I have an embedded cal.com link in my website for people to schedule calls with. My website has a dark theme so I set the javascript to initialize the theme in dark mode:

Cal.ns['30min']('ui', {
          theme: 'dark', // set theme to dark mode
          styles: { branding: { brandColor: '#3aaf4b' } },
          hideEventTypeDetails: false,
          layout: 'month_view',
        });

Occasionally when I load the page, the calendar will flicker between light and dark themes infinitely. It looks like the class value in the <html> tag keeps switching between class="light" and class="dark". Here is a video:

https://github.com/user-attachments/assets/64f4d89b-26e1-4990-972f-c873dc7ebaaa

I did a bit of testing and it appears to be looping between the theme that the javascript value has passed in (dark) and the theme of the browser (my browser is light theme). Even if they are both the same value (in my case dark and dark), my browser shows the value continuously being re-rendered. Watch the video below that shows the <html> objects class being continuously updated.

https://github.com/user-attachments/assets/65d0cafc-eeac-4cf8-b2c7-22d8a6f26041

Steps to Reproduce

Embed a cal.com month view in your website using the instructions in the web app and refresh the page enough times.

Expected behavior

The calendar should not flicker.

Other information

No response

Screenshots

No response

Environment

Desktop (please complete the following information)

- Devices: Macbook Pro (16inch) running Mac OS Sonoma and a M1 Macbook Pro
- Browsers: Multiple Chromium browsers including Google Chrome and Microsoft Edge
- The calendar was embedded using the vanilla JS script provided by cal.com's website.

      <div
        style="width: 100%; height: 100%; overflow: scroll"
        id="my-cal-inline"
      ></div>
      <script type="text/javascript">
        (function (C, A, L) {
...

From SyncLinear.com | CAL-4095

Souptik2001 commented 2 months ago

Was not able to replicate the issue. 🤔

nzayatz14 commented 2 months ago

@Souptik2001 yea its not super easy to replicate it. It took me about 6 hours of working on the page after I was first shown the issue to see it on my machine.

After digging through the code a bit, I think it may have something to do with this block in embed.ts

let currentColorScheme: string | null = null;

(function watchAndActOnColorSchemeChange() {
  // TODO: Maybe find a better way to identify change in color-scheme, a mutation observer seems overkill for this. Settle with setInterval for now.
  setInterval(() => {
    const colorScheme = getColorScheme(document.body);
    if (colorScheme && colorScheme !== currentColorScheme) {
      currentColorScheme = colorScheme;
      // Go through all the embeds on the same page and update all of them with this info
      CalApi.initializedNamespaces.forEach((ns) => {
        const api = getEmbedApiFn(ns);
        api("ui", {
          colorScheme: colorScheme,
        });
      });
    }
  }, 50);
})();

Maybe somehow this function gets called twice, and 2 intervals get set because neither get cancelled and it just bops back and forth? Once it happens again ill try to set a breakpoint and see if thats the case.

PeerRich commented 2 months ago

thats the disco mode

just kidding

lemme try to find someone to fix this

imharrisonking commented 2 months ago

Hi both,

I'm using NextJS and the React version of the inline embed and having the same issue in both dev and prod environments but only when opening up 2 or more instances of the same page on the same device. Doesn't seem to cause an issue in production testing the same page route of separate devices.

Here's a loom of the behaviour I'm getting: https://www.loom.com/share/283f6cd08e8941f18e4e1d9a95cd9480?sid=64f89ba4-06c4-4583-af2e-697072127c17

@nzayatz14 does this only occur when you have two instances of the page open during development?

Here's my code for the calendar component:

'use client';

import { useEffect } from 'react';
import Cal, { getCalApi } from '@calcom/embed-react';

export default function BookingCalendar() {
    const calURL = 'example/call';

    useEffect(() => {
        (async function () {
            const cal = await getCalApi();
            cal('ui', {
                theme: 'dark',
                styles: { branding: { brandColor: '#24332B' } },
                hideEventTypeDetails: false,
                layout: 'month_view',
            });
        })();
    }, []);

    return (
        <Cal
            calLink={calURL}
            style={{
                width: '100%',
                height: '100%',
                overflow: 'scroll',
            }}
            config={{ layout: 'month_view' }}
        />
    )
}
sbougerel commented 2 months ago

Hi,

I have the same issue, also using NextJS and the react embed code given by the application, using the pop-up method. Here is how you can reproduce the issue:

$ npx create-next-app@latest calcom-flicker-issue # Keep the default settings
✔ Would you like to use TypeScript? … No / **Yes**
✔ Would you like to use ESLint? … No / **Yes**
✔ Would you like to use Tailwind CSS? … No / **Yes**
✔ Would you like to use `src/` directory? … **No** / Yes
✔ Would you like to use App Router? (recommended) … No / **Yes**
✔ Would you like to customize the default import alias (@/*)? … **No** / Yes
...
$ cd calcom-flicker-issue
$ npm install @calcom/embed-react

Replace the content of app/page.tsx with:

import { Suspense } from "react";
import { CalComButton } from "./calcomButton";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <Suspense>
        <CalComButton />
      </Suspense>
    </main>
  );
}

Create the file app/CalComButton.tsx with:

"use client";

import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";

export function CalComButton() {
  useEffect(() => {
    (async function () {
      const cal = await getCalApi();
      cal("ui", {
        theme: "light",
        styles: { branding: { brandColor: "#00ff00" } },
      });
    })();
  }, []);

  return <button data-cal-link="rick">Click me</button>;
}

Now you can run your local server with:

$ npm run dev

Navigating to localhost:3000 should show a single button on a black background.

At this point, the remaining steps are:

My default desktop theme is dark, so I pick the "light" theme in the file app/CalComButton.tsx. You might have to do the reverse to see the flicker. It takes 2 pop-ups for the flicker to show up, as explained in other comments.

Happy to provide more information if needed to help with reproducing the issue.

hariombalhara commented 2 months ago

For anyone facing this issue, could you try the following:

Set config.theme same as the theme you set through ui instruction.

So the code becomes something like this. Notice config param in Cal component being passed has theme now

'use client';

import { useEffect } from 'react';
import Cal, { getCalApi } from '@calcom/embed-react';

export default function BookingCalendar() {
    const calURL = 'example/call';

    useEffect(() => {
        (async function () {
            const cal = await getCalApi();
            cal('ui', {
                theme: 'dark',
                styles: { branding: { brandColor: '#24332B' } },
                hideEventTypeDetails: false,
                layout: 'month_view',
            });
        })();
    }, []);

    return (
        <Cal
            calLink={calURL}
            style={{
                width: '100%',
                height: '100%',
                overflow: 'scroll',
            }}
            config={{ layout: 'month_view' , theme:'dark'}}
        />
    )
}

When set through config, theme is passed via query param which makes it always available. Setting it via 'ui' instruction has somehow started getting delayed by the browser(so it seems) causing us to wrongly calculate the storageKey for next-theme

nzayatz14 commented 2 months ago

Hey @hariombalhara, my original post has that theme explicitly set

hariombalhara commented 2 months ago

@nzayatz14 I see it set only in "ui" instruction image I am talking about setting in config. For Vanilla JS/HTML popup it requires setting a data attribute on the element that triggers the popup.

data-cal-config='{"theme":"dark"}'

Screenshot 2024-08-06 at 5 40 27 PM
nzayatz14 commented 2 months ago

@hariombalhara ah ok, is there a version of this for the VanillaJS HTML standard embedding (no popup)?

<script type="text/javascript">
        (function (C, A, L) {
          let p = function (a, ar) {
            a.q.push(ar);
          };
          let d = C.document;
          C.Cal =
            C.Cal ||
            function () {
              let cal = C.Cal;
              let ar = arguments;
              if (!cal.loaded) {
                cal.ns = {};
                cal.q = cal.q || [];
                d.head.appendChild(d.createElement('script')).src = A;
                cal.loaded = true;
              }
              if (ar[0] === L) {
                const api = function () {
                  p(api, arguments);
                };
                const namespace = ar[1];
                api.q = api.q || [];
                if (typeof namespace === 'string') {
                  cal.ns[namespace] = cal.ns[namespace] || api;
                  p(cal.ns[namespace], ar);
                  p(cal, ['initNamespace', namespace]);
                } else p(cal, ar);
                return;
              }
              p(cal, ar);
            };
        })(window, 'https://app.cal.com/embed/embed.js', 'init');
        Cal('init', '30min', { origin: 'https://cal.com' });

        Cal.ns['30min']('inline', {
          elementOrSelector: '#my-cal-inline',
          calLink: '...',
          layout: 'month_view',
        });

        Cal.ns['30min']('ui', {
          theme: 'dark',
          styles: { branding: { brandColor: '#3aaf4b' } },
          hideEventTypeDetails: false,
          layout: 'month_view',
        });
      </script>
hariombalhara commented 2 months ago

Yeah, you pass config like this

Cal.ns['30min']('inline', {
          elementOrSelector: '#my-cal-inline',
          calLink: '...',
         config: {
              // Setting theme through config is synchronous because it is passed as query param. So, do it here as well for best experience. After setting this it is optional to set the theme using "ui" instruction
              theme: "dark"
            }
             .....
hariombalhara commented 2 months ago

Let me know if it fixes the issue for you.

nzayatz14 commented 2 months ago

I'll add it and keep an eye out, thanks! You may want to add that to the code you generate when clicking on the embed options from inside the cal.com web app. Right now it only uses the ui instruction.

Could the issue be that it is flopping back and forth between the value in the config (which may or may not be set) and the one set in the ui instruction?

hariombalhara commented 2 months ago

Yeah, I am working on it https://github.com/calcom/cal.com/pull/16042

Actually it wasn't really needed earlier and to keep the code concise, I didn't add it. But due to some timing issue that recently started happening, it has become a requirement now(it seems). I spent quite some time figuring out, what changed due to which the issue started coming and nothing has changed in the flow.

Somehow, either the UI postmessage is taking longer to execute actually or cal.com AppProvider is getting initialized much faster. These are the only 2 reasons the issue can come.

But anyway it is better to guarantee that theme is available when AppProvider initializes, so I am going to update the config anyway for all codes.

sbougerel commented 2 months ago

@hariombalhara I can confirm that for the embed, the change that you propsed earlier:

data-cal-config='{"theme":"dark"}'

Indeed fixes the issue. For reference, this is what the content of the file calcomButton.tsx becomes after the change:

"use client";

import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";

export function CalComButton() {
  useEffect(() => {
    (async function () {
      const cal = await getCalApi();
      cal("ui", {
        theme: "light",
        styles: { branding: { brandColor: "#00ff00" } },
      });
    })();
  }, []);

  return (
    <button data-cal-link="rick" data-cal-config='{"theme": "light"}'>
      Click me
    </button>
  );
}
imharrisonking commented 2 months ago

Hey @hariombalhara I just set the config.theme to be the same as the theme you set through ui instruction as you suggested.

Has fixed the issue for me, thanks!!