Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DateRange Broken on Mobile #13

Open
tylerbecks opened this issue Feb 19, 2025 · 0 comments
Open

DateRange Broken on Mobile #13

tylerbecks opened this issue Feb 19, 2025 · 0 comments

Comments

@tylerbecks
Copy link

tylerbecks commented Feb 19, 2025

The DateRange picker overflows on mobile, it should break into flex-col when small. I fixed this on my own app, thought I'd share:

Before After
Image Image

I'll abbreviate the code to highlight the changes

Calendar.tsx

import { DayPicker } from 'react-day-picker';
import { useBreakpoint } from '@/hooks/use-breakpoint';

function Calendar(props: Props) {
  const isMobile = useBreakpoint('SM');
  const columnsDisplayed = navView === 'years' ? 1 : numberOfMonths;

  return (
        <DayPicker
          style={{
            width: 248.8 * (columnsDisplayed && !isMobile ? columnsDisplayed : 1) + 'px',
          }}
          classNames={{
            months: cn('relative flex flex-col sm:flex-row', props.monthsClassName)
         }}
        />
  );
}

date-range-picker.tsx

This adds react-use-measure to adjust the top of the picker if it flows over the top of the page. It might also need to adjust the bottom, but I haven't run into this issue yet. Check the changes in PopoverContent

'use client';

import * as React from 'react';
import { format } from 'date-fns';
import { Calendar as CalendarIcon } from 'lucide-react';
import { DateRange } from 'react-day-picker';

import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import useMeasure from 'react-use-measure';

interface Props extends React.HTMLAttributes<HTMLDivElement> {
  dateRange: DateRange | undefined;
  onDateRangeChange: (dateRange: DateRange | undefined) => void;
}

export function DatePickerWithRange({ className, dateRange, onDateRangeChange }: Props) {
  const [ref, bounds] = useMeasure();

  return (
    <div className={cn('grid gap-2', className)}>
      <Popover>
        <PopoverTrigger asChild>
          <Button
            id="date"
            variant={'outline'}
            className={cn(
              'w-full justify-start text-left font-normal',
              !dateRange && 'text-muted-foreground',
            )}
          >
            <CalendarIcon className="mr-2 size-4" />
            {dateRange?.from ? (
              dateRange.to ? (
                <>
                  {format(dateRange.from, 'LLL dd, y')} - {format(dateRange.to, 'LLL dd, y')}
                </>
              ) : (
                format(dateRange.from, 'LLL dd, y')
              )
            ) : (
              <span>Add dates</span>
            )}
          </Button>
        </PopoverTrigger>
        <PopoverContent
          className="w-auto p-0"
          align="center"
          sideOffset={bounds.top < 0 ? bounds.top - 28 : undefined}
          ref={ref}
        >
          <Calendar
            autoFocus
            mode="range"
            defaultMonth={dateRange?.from}
            selected={dateRange}
            onSelect={onDateRangeChange}
            numberOfMonths={2}
          />
        </PopoverContent>
      </Popover>
    </div>
  );
}

Lastly, for completeness, here's my implementation of hooks/use-breakpoint.ts

hooks/use-breakpoint.tsx

'use client';
import { useEffect, useState } from 'react';

// Breakpoints from tailwind: https://tailwindcss.com/docs/responsive-design
export enum Breakpoint {
  SM = '640',
  MD = '768',
  LG = '1024',
  XL = '1280',
  '2XL' = '1536',
}

// Global subscription manager for resize events
const subscribers = new Set<(width: number) => void>();
let globalResizeObserver: ResizeObserver | null = null;

function initGlobalResizeObserver() {
  // Ensure that only one global observer is created
  if (globalResizeObserver === null && typeof document !== 'undefined') {
    globalResizeObserver = new ResizeObserver((entries) => {
      const width = entries[0].contentRect.width;
      subscribers.forEach((callback) => callback(width));
    });
    globalResizeObserver.observe(document.body);
  }
}

/**
 * Hook to check if the current viewport is below a given breakpoint using a global ResizeObserver
 * @param breakpoint The breakpoint to check against (in pixels)
 * @returns boolean indicating if the viewport is below the breakpoint
 */
export const useBreakpoint = (breakpoint: keyof typeof Breakpoint) => {
  const [isBelow, setIsBelow] = useState(false);

  useEffect(() => {
    const checkBreakpoint = (width: number) => {
      setIsBelow(width < Number(Breakpoint[breakpoint]));
    };

    // Initialize the global resize observer if it hasn't been already
    initGlobalResizeObserver();

    // Add this instance's callback to the subscribers
    subscribers.add(checkBreakpoint);

    // Ensure the current width is applied immediately
    checkBreakpoint(window.innerWidth);

    return () => {
      // Clean up by removing this callback from subscribers on unmount
      subscribers.delete(checkBreakpoint);
    };
  }, [breakpoint]);

  return isBelow;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant