uvarov-frontend / vanilla-calendar-pro

Vanilla Calendar is a versatile JavaScript date and time picker with TypeScript support, making it compatible with any JavaScript framework or library. It is designed to be lightweight, simple to use, and feature-rich without relying on external dependencies.
https://vanilla-calendar.pro
MIT License
394 stars 53 forks source link

Responsive Layout - pop-up negatively interfering #235

Open uvarov-frontend opened 4 months ago

uvarov-frontend commented 4 months ago

Discussed in https://github.com/uvarov-frontend/vanilla-calendar-pro/discussions/176

Originally posted by **pablopereira27** December 30, 2023 For small screens (Boostrap sm ceil breakpoint = 767.98px), I applied the following change to the multiple month calendar. `@media (max-width: 767.98px) { .vanilla-calendar_multiple { width: 100%; max-width: 550px; } }` If it weren't for the pop-ups that can go out the calendar, it would be great. ![image](https://github.com/uvarov-frontend/vanilla-calendar-pro/assets/11337781/81352d59-749f-41bd-a3fd-00301e84af7d) ![image](https://github.com/uvarov-frontend/vanilla-calendar-pro/assets/11337781/ef782339-89e0-4fa8-8128-6f9243b542ae) `overflow: hidden` in the `.calendar` prevents the entire site from being affected, but prevents the user from seeing pop-ups that exceed the calendar. ![image](https://github.com/uvarov-frontend/vanilla-calendar-pro/assets/11337781/00af1d93-37fa-4b30-a203-8927d8388bea) ![image](https://github.com/uvarov-frontend/vanilla-calendar-pro/assets/11337781/fff70b75-52ae-441b-ab63-379407010042) I couldn't find a way to solve it via CSS. Could you help me with any tips or any future adjustments to the calendar javascript to avoid this leak of calendar pop-ups?
tfsumon commented 4 months ago

Hi @uvarov-frontend,

I encountered an issue where the entire calendar (not just a popup) was overflowing outside the body container at a specific breakpoint (1070px).

I tried fixing it with CSS initially, but wasn't successful. I then implemented a JavaScript solution that resolved the problem.

See the original issue in this example: website (1070px breakpoint).

Here's the fixed version for your reference: website

Here is the JavaScript code I used for the fix.

const options = {
  input: true,
  actions: {
    changeToInput(e, calendar, self) {
      if (!self.HTMLInputElement) return;
      if (self.selectedDates[0]) {
        self.HTMLInputElement.innerHTML = self.selectedDates[0];

        // if you want to hide the calendar after picking a date
        calendar.hide();
      } else {
        self.HTMLInputElement.innerHTML = "Select Date";
      }
    },
    showCalendar(self) {
      const setPositionCalendar = (
        input,
        calendar,
        position,
        css,
      ) => {
        const getPosition = {
          top: -calendar.offsetHeight,
          bottom: input.offsetHeight,
          left: 0,
          center: input.offsetWidth / 2 - calendar.offsetWidth / 2,
          right: input.offsetWidth - calendar.offsetWidth,
        };

        const YPosition = !Array.isArray(position)
          ? "bottom"
          : position[0];
        const XPosition = !Array.isArray(position)
          ? position
          : position[1];

        let left = input.offsetLeft;
        let top = input.offsetTop;

        // Calculate document dimensions
        const docWidth = document.documentElement.clientWidth;
        const docHeight = document.documentElement.clientHeight;

        // Calculate window scroll offsets
        const scrollLeft =
          window.scrollX || document.documentElement.scrollLeft;
        const scrollTop =
          window.scrollY || document.documentElement.scrollTop;

        // Calculate calendar width and set maximum width to 100%
        const calendarWidth = Math.min(
          calendar.offsetWidth,
          docWidth,
        );

        // Check if there's horizontal overflow
        const rightOverflow =
          left + calendarWidth > docWidth + scrollLeft;
        const leftOverflow = left < scrollLeft;

        // Check if there's vertical overflow
        const verticalOverflow =
          (YPosition === "bottom" &&
            top + input.offsetHeight + calendar.offsetHeight >
              docHeight + scrollTop) ||
          (YPosition === "top" &&
            top - calendar.offsetHeight < scrollTop);

        // Adjust positions accordingly
        if (rightOverflow && XPosition !== "left") {
          left = input.offsetLeft + input.offsetWidth - calendarWidth;
        } else if (leftOverflow && XPosition !== "right") {
          left = input.offsetLeft;
        }

        // Center the calendar if the left or right value is negative
        if (left < 0 || left + calendarWidth > docWidth) {
          left =
            document.documentElement.offsetLeft +
            (document.documentElement.offsetWidth - calendarWidth) /
              2;
        }

        if (verticalOverflow) {
          top =
            YPosition === "bottom"
              ? input.offsetTop - calendar.offsetHeight
              : input.offsetTop + input.offsetHeight;
        } else {
          top =
            YPosition === "bottom"
              ? top + input.offsetHeight
              : top - calendar.offsetHeight;
        }

        calendar.classList.add(
          YPosition === "bottom"
            ? css.calendarToInputBottom
            : css.calendarToInputTop,
        );

        Object.assign(calendar.style, {
          left: `${left}px`,
          top: `${top}px`,
          maxWidth: "100%",
        });
      };

      setPositionCalendar(
        self.HTMLInputElement,
        self.HTMLElement,
        "auto",
        self.CSSClasses,
      );

      const actionsInput = (self) => ({
        hide() {
          self.HTMLElement.classList.add(
            self.CSSClasses.calendarHidden,
          );
          if (self.actions.hideCalendar)
            self.actions.hideCalendar(self);
        },
        show() {
          self.HTMLElement.classList.remove(
            self.CSSClasses.calendarHidden,
          );
          if (self.actions.showCalendar)
            self.actions.showCalendar(self);
        },
        self,
      });

      const documentClickEvent = (e) => {
        if (
          !self ||
          e.target === self.HTMLInputElement ||
          self.HTMLElement?.contains(e.target)
        )
          return;

        if (self.HTMLInputElement && self.HTMLElement)
          actionsInput(self).hide();
        window.removeEventListener("resize", handleResize);
        document.removeEventListener("click", documentClickEvent, {
          capture: true,
        });
      };

      const handleResize = () =>
        setPositionCalendar(
          self.HTMLInputElement,
          self.HTMLElement,
          "auto",
          self.CSSClasses,
        );

      self.HTMLInputElement.addEventListener("click", () => {
        window.addEventListener("resize", handleResize);
        document.addEventListener("click", documentClickEvent, {
          capture: true,
        });
      });

      document.addEventListener("click", documentClickEvent, {
        capture: true,
      });

      window.addEventListener("resize", handleResize);
      window.addEventListener("scroll", handleResize);
    },
  },
whataboutpereira commented 4 months ago

Probably easier to use Floating UI for positioning.

uvarov-frontend commented 4 months ago

@tfsumon Thanks for sharing, I’ll think about making the calendar auto-position depending on the breakpoint. But it was easier for you to use Floating UI, as already written above.

ghiscoding commented 1 month ago

I have the code for auto-positioning in a fork, I can submit a PR for auto-positioning after all my other PRs are reviewed/merged :)

I also assume that my code could help with the original issue since I have a new function that given an element, will calculate the best possible side to reposition itself (by available space depending on element's position in the viewport).

Cheers