MUI DateCalendar With Luxon Adapter Start Week On Sunday

454 views Asked by At

Recently, Luxon added support for localized week information. I updated my version of Luxon to the latest release that contains this update, 3.4.4 but when opening up the calendar, the week still starts on Monday instead of Sunday.

I would assume that the LuxonAdapter would pick up the changes and use the localized week data but it doesn't seem to be any different.

My package.json has the latest versions of both libraries:

"@mui/x-date-pickers": "^7.0.0-alpha.0",
"luxon": "^3.4.4",

Then, in my App.tsx I add the LocalizationProvider with the LuxonAdapter:

import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterLuxon } from "@mui/x-date-pickers/AdapterLuxon";

<LocalizationProvider dateAdapter={AdapterLuxon}>
    <Outlet />
</LocalizationProvider>

Lastly, the DateCalendar component:

import { DateTime } from "luxon";
import { DateCalendar } from "@mui/x-date-pickers";

const [calendarValue, setCalendarValue] = useState<DateTime | null>(DateTime.now());

<DateCalendar
    value={calendarValue}
    onChange={setCalendarValue}
    views={["day"]}
    disablePast
    sx={{ m: 0 }}
/>
1

There are 1 answers

1
VonC On BEST ANSWER

I see you currently have:

Client Application
│
├─ package.json
│  ├─ "@mui/x-date-pickers": "^7.0.0-alpha.0"
│  └─ "luxon": "^3.4.4"
│
├─ App.tsx
│  ├─ LocalizationProvider (with LuxonAdapter)
│  └─ Outlet
│
└─ DateCalendar Component
   ├─ DateTime from "luxon"
   └─ DateCalendar from "@mui/x-date-pickers"

Verify first that Luxon is configured with the correct locale that starts the week on Sunday. If your locale is set to one where the week traditionally starts on Monday, Luxon will follow that setting.

import { Settings } from 'luxon';

// Set the locale globally for Luxon
Settings.defaultLocale = 'your-locale'; // Replace 'your-locale' with the appropriate locale code

If the LuxonAdapter does not automatically pick the start of the week from Luxon settings, you might need to configure it explicitly. But I do not see a way to do this directly in @mui/x-date-pickers.

mui/material-ui issue 30591 proposes, on a similar issue referenced by moment/luxon issue 1447 (the very issue your moment/luxon PR 1454 fixes)

function adapterLuxonFactory(weekStartsOn: number) {
  class Adapter extends AdapterLuxon {
    private weekStartsOn = weekStartsOn;

    /** Controls the header of the calendar month view. */
    public getWeekdays = () => {
      const weekdays = Info.weekdaysFormat("narrow", { locale: this.locale });
      const start = weekdays.slice(0, this.weekStartsOn - 1);
      const end = weekdays.slice(this.weekStartsOn - 1);
      return [...end, ...start];
    };

    /** Controls the day buttons of the calendar month view. */
    getWeekArray = (date: DateTime) => {
      const startOfMonth = date.startOf("month");
      const endOfMonth = date.endOf("month");
      const firstDayOfMonth = startOfMonth.weekday;

      const startOfWeek = startOfMonth.minus({
        days: (firstDayOfMonth - this.weekStartsOn + 7) % 7,
      });

      const endOfWeek = endOfMonth
        .plus({ days: 6 - endOfMonth.weekday + this.weekStartsOn })
        .endOf("day");

      const { days } = endOfWeek.diff(startOfWeek, "days").toObject();

      const weeks: DateTime[][] = [];
      new Array<number>(Math.round(days ?? 0))
        .fill(0)
        .map((_, i) => i)
        .map((day) => startOfWeek.plus({ days: day }))
        .forEach((v, i) => {
          if (i === 0 || (i % 7 === 0 && i > 6)) {
            weeks.push([v]);
            return;
          }

          weeks[weeks.length - 1].push(v);
        });

      return weeks;
    };
  }

  return Adapter;
}

export function LocalizationProvider({
  children,
}: {
  children?: React.ReactNode;
}): JSX.Element {
  const { weekStartsOn } = useYourOwnDatabase();

  return (
    <MuiLocalizationProvider dateAdapter={adapterLuxonFactory(weekStartsOn)}>
      {children}
    </MuiLocalizationProvider>
  );
}

The custom adapter factory function adapterLuxonFactory takes a weekStartsOn parameter (number representing the day the week starts on, with 1 for Monday, 2 for Tuesday, up to 7 for Sunday). That function is used in the LocalizationProvider to make sure the entire calendar component respects the specified start of the week.

To integrate this solution into your current setup, you would replace, for testing, the standard LuxonAdapter with this custom adapter, by modifying the LocalizationProvider in your App.tsx to use this custom adapter.
Make sure you provide the correct weekStartsOn value when initializing the adapter. For starting the week on Sunday, this would be 7.


I also see mui/mui-x issue 9984, which says:

Since 2021, I've been using the workaround noted in mui/material-ui-pickers issue 1270 to force Luxon to use Sunday as the first day of the week and just noticed that it no longer works.

import {Info} from 'luxon';
import LuxonUtils from '@date-io/luxon';

class CustomLuxonUtils extends LuxonUtils {
  getWeekdays() {
    // need to copy the existing, and use Info to preserve localization
    const days = [...Info.weekdaysFormat('narrow', this.locale)];
    // remove Sun from end of list and move to start of list
    days.unshift(days.pop());
    return days;
  }

  getWeekArray(date) {
    const endDate = date
      .endOf('month')
      // if a month ends on sunday, luxon will consider it already the end of the week
      // but we need to get the _entire_ next week to properly lay that out
      // so we add one more day to cover that before getting the end of the week
      .plus({days: 1})
      .endOf('week');
    const startDate = date
      .startOf('month')
      .startOf('week')
      // must subtract 1, because startOf('week') will be Mon, but we want weeks to start on Sun
      // this is the basis for every day in a our calendar
      .minus({days: 1});

    const {days} = endDate.diff(startDate, 'days').toObject();

    const weeks = [];
    new Array(Math.round(days))
      .fill(0)
      .map((_, i) => i)
      .map(day => startDate.plus({days: day}))
      .forEach((v, i) => {
        if (i === 0 || (i % 7 === 0 && i > 6)) {
          weeks.push([v]);
          return;
        }

        weeks[weeks.length - 1].push(v);
      });

    // a consequence of all this shifting back/forth 1 day is that you might end up with a week
    // where all the days are actually in the previous or next month.
    // this happens when the first day of the month is Sunday (Dec 2019 or Mar 2020 are examples)
    // or the last day of the month is Sunday (May 2020 or Jan 2021 is one example)
    // so we're only including weeks where ANY day is in the correct month to handle that
    return weeks.filter(w => w.some(d => d.month === date.month));
  }
}

I was able to fix it by adding an override for the startOfWeek method as follows:

startOfWeek = (value: DateTime) => {
    return value.startOf('week').minus({ days: 1 });
}

This is discussed in mui/mui-x issue 10805: "[pickers] Support changing start of week day on AdapterLuxon"

The clean fix will be merged in the v7 alpha branch.

This is mui/mui-x PR 10964