base-ui
base-ui copied to clipboard
[temporal] New `Calendar` component
Part of #1709
Step 1: Extract a minimal version of the Calendar component from the X repo
In this PR
Adapters
~The adapters expose a unified set of methods in order to support several date libraries. The MUI X Date and Time Pickers currently support 8 adapters (Day.js, Luxon, Moment, Date Fns v3+v4, Date Fns v2, Moment Jalaali, Moment Hijri and Date Fns Jalali). One of the main goal of this PR should be to decide if we want to keep this abstraction.~
Decided to hide the adapter initially and use the date-fns adapter internally.
- [x] Import the adapter internals
- [x] Remove the properties only used by the field components (to keep the PR "light")
- [x] Improve some methods (create a
adapter.now()method to avoid polymorphism onadapter.date()) - [x] Remove most unused formats and give more explicit names to the remaining ones (to know if they have leading zeros or not)
- [x] Import and update the Luxon adapter
- [x] Migrate the adapter tests
Manager / validation
The manager expose methods to interact with the value regarless of its type (date, time, date time, date range, time range and date time range).
This allow to do stuff like validate the value, set the timezone on a value, compare two values etc...
It allows to share a lot of code between the Calendar and the Range Calendar (see useSharedCalendarRoot).
On the field components, it will allow to have almost all the code agnostic of the value type and to create very light wrappers for each value type.
- [x] Import the manager internals
- [x] Remove the properties the the calendar doesn't use (to keep the PR "light")
- [x] Import and update the date manager
- [x] Import the validation internals
- [x] Import and update the date validation
- [x] Rework the date validation API (remove
disableFutureanddisablePast, replaceshouldDisableDatewith aisDateUnavailablethat is not part of the validation per say like on React Aria) - [x] Add tests
Calendar component
- [x] Migrate my PoC from the MUI X repo without the month view, the year view and the range calendar parts
- [x] Use the new store/selector introduced for the Select
- [x] Create a first version of the doc page with a CSS Module example
- [x] Create a few experiments
- [x] Implement a11y keyboard navigation
- [ ] Refine keyboard shortcut navigation with checks for disabled days
- [x] Fix issues with typing for DOM updates/syncs (
onKeyDown)
- [ ] Add example of lazy loading inside of days
- [ ] Add Tailwind examples
- [ ] Add tests (not sure how deep I'll go :laughing: )
- [x] Update to use the new
eventDetailsstructure - [x] Add transitions to month navigation
Deploy Preview for base-ui ready!
| Name | Link |
|---|---|
| Latest commit | 744b4ee20d447107450e06c80730ce123e5d7fe4 |
| Latest deploy log | https://app.netlify.com/projects/base-ui/deploys/691dcab359ca260008d0d6d3 |
| Deploy Preview | https://deploy-preview-1973--base-ui.netlify.app |
| Preview on mobile | Toggle QR Code...Use your smartphone camera to open QR code link. |
To edit notification comments on pull requests, go to your Netlify project configuration.
-
pnpm add https://pkg.pr.new/mui/base-ui/@base-ui-components/react@1973pnpm add https://pkg.pr.new/mui/base-ui/@base-ui-components/utils@1973
commit: f4b65de
Bundle size report
| Bundle | Parsed size | Gzip size |
|---|---|---|
| @base-ui-components/react | ๐บ+17KB(+5.44%) | ๐บ+4.81KB(+4.83%) |
Generated by :no_entry_sign: dangerJS against f3c2aa59bb5cd600e163588b02392b8d8f5750ce
I'm wondering if a template-based API (not fully composable, so instead of forcing users to render each week and day, they'd only specify a template and we will be responsible for rendering) wouldn't be a better choice here.
I suppose (feel free to correct me, though) that in most (if not all) cases, users will simply map weeks and days arrays to DayGridRow and DayGridCell. Having a DayTemplate and WeekTemplate could simplify the required JSX.
@michaldudak could you give me a basic JSX example of what it could look like?
Something along these lines:
<Calendar.DayGridBody className={styles.DayGridBody}>
<Calendar.DayGridRowTemplate>
{ (week, props) => <div {...props} className={styles.DayGridRow} /> }
</Calendar.DayGridRowTemplate>
<Calendar.DayGridCellTemplate>
{ (day, props) => <div {...props} className={styles.DayGridCell}><Calendar.DayButton className={styles.DayButton} /></div> }
</Calendar.DayGridCellTemplate>
</Calendar.DayGridBody>
(alternatively, with a render prop instead of children)
EDIT: I just realized this could be problematic with SSR, as templates would have to register themselves in their parent. We could work around this by looking for them with React.children.
I have mixed feelings with the shortcuts on the DX here. Is the few lines of JSX removed worth the new concept that people will have to learn? That's always the question I guess.
In your example, how would people style the other nodes? (mostly GridHeader, GridHeaderBody and GridHeaderCell?)
In my example, I think I'm adding CSS to every single DOM node.
Or how they would pass other props like event handlers? (probably more niche for sure).
I do agree with you that for bigger component, the fully composable approach becomes quite verbose. And that, for the Calendar, almost all app will have similar JSX if they use a fully composable approach. I do think the fully composable approach has value and I wouldn't remove it (because it is the most flexible one). But I'm interested to see how we could come up with a alternative and complementary DX. React Aria has a short version:
<CalendarGrid>
{(date) => <CalendarCell date={date} />}
</CalendarGrid>
But it only works because they ship CSS by default.
We could do the same and expose a classes prop to allow people to style the nodes, but it's not super "Base UI".
Another point, I decided to let the user map the cells, to be super flexible, but we could of course discuss replacing:
<Calendar.DayGridRow value={week}>
{({ days }) =>
days.map((day) => (
<Calendar.DayGridCell value={day}>
<Calendar.DayButton />
</Calendar.DayGridCell>
))
}
</Calendar.DayGridRow>
With:
<Calendar.DayGridRow value={week}>
{({ day }) =>
<Calendar.DayGridCell value={day}>
<Calendar.DayButton />
</Calendar.DayGridCell>
}
</Calendar.DayGridRow>
It's what React Aria does on their Date Field component You loose the ability to skip some days, add stuff between the days etc... (which I don't have a valid use case for, but I can't guarantee nobody wants to do it), but you have a shorter DX. (same the the header cell and the grid row)
@flaviendelangle Thanks for this, excited to see it! I had a quick first look over it.
Chiming in on the recent feedback
I'm wondering if a template-based API (not fully composable, so instead of forcing users to render each week and day, they'd only specify a template and we will be responsible for rendering) wouldn't be a better choice here. Is the few lines of JSX removed worth the new concept that people will have to learn?
I don't personally feel much pain in the JSX. On the contrary, I'm actually quite surprised at how terse it is. If there are new concepts to learn, or potential issues with SSR, or styling limitations, or content limitations, I'd opt for the usual normal compound pattern. If there are none of those limitations, it's worth considering.
I decided to let the user map the cells, to be super flexible, but we could of course discuss replacing:
This seems more promising to me. I prefer it without the map. If we can't think of a valid use case we'd be restricting, it seems we should do this?
Some questions/suggestions of my own:
- In June, why are dates 6โ10 missing? Can I control that somehow?
- Can you add
isDateUnavailableandtimezoneto.Rootin the demo please? Just so I can see the syntax. - What is the thinking behind exposing
<Calendar.KeyboardNavigation>as a component, rather than just always enabling keyboard navigation and not having a component for it? - Drop "Day" and just call it
Calendar.Grid? - What is the purpose of the
nativeButtonprop and what's its use case? - Rather than
<Calendar.SetVisibleMonth target="previous|next", what do you think aboutCalendar.PreviousandCalendar.Next? OrCalendar.PreviousMonthandCalendar.NextMonthorCalendar.SetPreviousandCalendar.SetNext. I think something like this might be more inline with the conventions we set in our NumberField component. - Do we have some mechanism to set the beginning of the week? Some people consider it Sunday, others Monday.
- How will animation work? Do we need to add the
data-starting-styleanddata-ending-styleattrs? - How could we animate it vertically, so the next/previous buttons are up/down arrow icons, and the next month slides up from the bottom, and the previous month slides down from the top?
- What is the purpose of the
{({ weeks }) =>map? I was expecting to just see the days map, since I don't see any mention of weeks in the calendar demo.
Btw, lmk whenever you want some styling feedback. I assumed it's too early for that, so didn't bother.
Feedback
- Bug?: I'm assuming focus isn't meant to revert back to the beginning after navigating to a new month and selecting a day
https://github.com/user-attachments/assets/9c057afa-d9ed-4409-a8b4-1f7c02b9c13b
- What's the purpose of
Calendar.KeyboardNavigation? Shouldn't this be implicit inside theRootpart?
- VoiceOver doesn't announce the new month when navigating down continually (past the last/first date) once it loops. The buttons also don't have an
aria-labeland say the icon name.
This seems more promising to me. I prefer it without the map. If we can't think of a valid use case we'd be restricting, it seems we should do this?
I will remove the map so that we can discuss this version :+1:
In June, why are dates 6โ10 missing? Can I control that somehow?
I will check why
Can you add isDateUnavailable and timezone to .Root in the demo please? Just so I can see the syntax.
Sure
What is the thinking behind exposing <Calendar.KeyboardNavigation> as a component, rather than just always enabling keyboard navigation and not having a component for it?
What's the purpose of Calendar.KeyboardNavigation? Shouldn't this be implicit inside the Root part?
It did the split recently looking at #1963, but I'm very unsure My reasoning was to have a smaller bundle size when you don't need keyboard navigaton, but I struggle to find a use case where it doesn't make sense to have it...
What is the purpose of the nativeButton prop and what's its use case?
I just re-used what other components are doing when using useButton
I think it's to support SSR correctly.
Rather than <Calendar.SetVisibleMonth target="previous|next", what do you think about Calendar.Previous and Calendar.Next? Or Calendar.PreviousMonth and Calendar.NextMonth or Calendar.SetPrevious and Calendar.SetNext. I think something like this might be more inline with the conventions we set in our NumberField component.
No big technical challenge here. If we are fine having 6 components it's good for me. I just think having a shortcut way of going to the previous and next without date manipulation is nice. But not strong preference on the exact DX.
Do we have some mechanism to set the beginning of the week? Some people consider it Sunday, others Monday.
We let the date library handle that. I need to document it, you can find the MUI X doc here
How will animation work? Do we need to add the data-starting-style and data-ending-style attrs?
I did not explore that at all yet. Do you think it's needed on the initial PR or can it be kept for a follow up?
How could we animate it vertically, so the next/previous buttons are up/down arrow icons, and the next month slides up from the bottom, and the previous month slides down from the top?
For me the current version make no assumption on the rendering layout to we could animate vertically. But I would need to explore and add experiments.
What is the purpose of the {({ weeks }) => map? I was expecting to just see the days map, since I don't see any mention of weeks in the calendar demo.
Not sure to understand this one.
Do you mean you expected a single map in ยทCalendar.DayGridBody that would render the days without having one row per week?
Bug?: I'm assuming focus isn't meant to revert back to the beginning after navigating to a new month and selecting a day
Clearly bug EDIT: Should be fixed now
VoiceOver doesn't announce the new month when navigating down continually (past the last/first date) once it loops. The buttons also don't have an aria-label and say the icon name.
The accessibility is probably far from perfect for now. I will add aria-label on the buttons. For the new month, not sure how to achieve that.
Drop "Day" and just call it Calendar.Grid?
The naming was picked when the calendar supported month and year navigation. If you think we shouldn't support those views then clearly we can simplify the names of most parts. If you think we should support those views, then either we keep the prefix for clarity or we say that the day is by far the most widely used view so we can skip the prefix for this one.
@atomiks React Aria writes "Next" as the aria label of the button that goes to the next month. To do something similar we would need a localization support inside our component. I can write the targetted date (since it's formatted by the date library I do have localization with dates). WDYT?
@flaviendelangle For NumberField buttons it defaults the aria-label to English (Increase/Decrease) and the expectation is that aria-label is manually set for localization on the button component
I wonder if the demos should set it manually so it becomes more obvious it needs to be localized
I'll add "Next" / "Previous" labels then :ok_hand:
By the way, I'll remove the year navigation button for now. There is a use case where people want to have a double right arrow to navigate year per year (Ant Design has it), but it's easy to implement in userland so I don't think it's worth the added complexity in our codebase, unless we do the month view of course.
@colmtuite I split the SetVisibleMonth part: SetMonth SetPreviousMonth and SetNextMonth
My main concern with this naming is that we loose the information that it only navigate through the months but it never actually sets any value.
That's why I had Visible before.
Maybe we could consider a naming like GoToPreviousMonth that makes this distinction clearer.
@colmtuite for the timezone,it won't change anything visually, it has 2 impacts:
- It defines which timezone will be used if you don't have any value / default value / reference date provided.
- It allows to set a different rendering timezone that the one of the value (so for the Calendar, you might have a value that is on the 5th of June, but see the 6th of June selected in your rendering timezone)
I added it to the demo so you can see the DX
EDIT: Demo added here
With the useWeekList and useDayList it's easier than I expected.
Btw, lmk whenever you want some styling feedback. I assumed it's too early for that, so didn't bother.
I think we can discuss doc design once we have somewhat stabilized the JSX structure, so I don't spend too much time migrating stuff whenever I break the API :laughing:
@atomiks I have one big accessibility question: should we use table, thead etc... or keep div with additional aria roles?
React Aria uses the table, MUI X uses divs.
The downside of the React Aria approach is that, with full composition we would need an additional Calendar.DayGridHeaderRow because they have table => thead => tr => th whereas we have just Root => DayGridHeader => DayGridHeaderCell.
But other than that, going their way might improve the accessibility. It allow us to remove aria-rowindex and aria-colindex
@colmtuite I'm now remembering the reason of the explicit map :facepalm: It allows to easily add the week number: https://deploy-preview-16069--material-ui-x.netlify.app/x/react-date-pickers/base-calendar/#recipe-with-week-number
With the implicit map, it's a lot harder to do it.
It's probably not totally impossible, people could use useWeekList and useDayList directly and pass a React Node instead of a render prop to own the JSX structure.
In June, why are dates 6โ10 missing? Can I control that somehow?
I can't reproduce this If you could give me more info :pray:
I have one big accessibility question: should we use table, thead etc... or keep div with additional aria roles? React Aria uses the table, MUI X uses divs.
Not sure here as each one seems to have trade-offs; but I think an extra JSX node may be worth it if it's more widely accessible and removes complexity internally. If they're equivalent otherwise, removing an extra wrapper could be worth it, though.
To be honest I don't have the full picture here, accessibility is not my biggest strength :grimacing:
@flaviendelangle role="grid" + role="gridcell" should be equivalent to the <table> provided the attributes and interactions are wired correctly.
https://adrianroselli.com/2020/07/aria-grid-as-an-anti-pattern.html Grids are used for interactive widgets while tables for regular tabular data that isn't interactive, so it looks like a correct fit.
From testing out the component with VoiceOver and comparing other calendars in the wild:
- When entering the calendar header, it should announce the current month. This might be achieved by
aria-labelon the root or maybearia-labelledbylinking the button to the month text - When entering the day grid, it correctly announces the number of rows and columns as a table, but it should also announce the current focused day as "Today" plus the date (see dot point below)
- When navigating to a new day, it shouldn't just announce the number. It should announce the full date e.g.
"Thursday, June 5th, 2025" - When selecting a day, it should announce the day (possibly with a
role="status"announcer) as"Selected: Thursday, June 5th, 2025"
- When navigating to a new day, it shouldn't just announce the number. It should announce the full date e.g. "Thursday, June 5th, 2025"
For this one, I guess I can just add an aria-label with a nicely formatted date on every Calendar.DayButton
I'll try to solve those :ok_hand:
@flaviendelangle
I will remove the map so that we can discuss this version ๐
Great.
In June, why are dates 6โ10 missing? Can I control that somehow?
I meant 6โ10 July is missing from the June view. Apologies, I should have clarified. I think ideally there would be a way to set whether the grid includes rows for the
Calendar.KeyboardNavigation My reasoning was to have a smaller bundle size when you don't need keyboard navigaton, but I struggle to find a use case where it doesn't make sense to have it...
I can't think of a reason either. Imo we should remove it and have keyboard navigation be mandatory.
it only navigate through the months but it never actually sets any value Maybe we could consider a naming like GoToPreviousMonth that makes this distinction clearer.
The "GoTo" and "Set" seem kinda redundant to me. How about just .PreviousMonth and .NextMonth? If week, month, and year never need to be rendered in the same view (such that we need to differentiate between triggers), I'd even shorten it to just .Next and .Previous.
How will animation work? Do you think it's needed on the initial PR or can it be kept for a follow up?
It's necessary for the initial release, but I've no preference on when we tackle it.
we could animate vertically. But I would need to explore and add experiments.
Ok cool. This will be necessary for the initial release. But again, can tackle it whenever.
I'll remove the year navigation button for now. it's easy to implement in userland so I don't think it's worth the added complexity in our codebase
What does this look like in user land? MUI has <Calendar.SetVisibleYear /> which seems good? I guess that would mean:
<Calendar.PreviousMonth />
<Calendar.NextMonth />
<Calendar.PreviousYear />
<Calendar.NextYear />
or perhaps:
<Calendar.Previous scope="month" />
<Calendar.Next scope="month" />
<Calendar.Previous scope="year" />
<Calendar.Next scope="year" />
month and year navigation
I need some time to research these month and year views and think about it.
New thoughts
- How does SetMonth work? Is it just for setting the month? Or could I use it to create buttons for navigating to the month containing today, yesterday, and last/next week?
- MUI has this calendar where it has a weird dropdown thing that opens the year view. Imo, this should just be a Select. Or perhaps two selects beside each other, one for month, one for year. I'm thinking it would just be straight Base UI Select, you just compose the primitives yourself. Would we need to expose a hook or something for this, to update the calendar via Select's
onValueChange? (or however code works) - This is all getting complex quickly. So I'm going to put together a shaping doc in Notion over the weekend, where I'll collect research notes.
How does SetMonth work? Is it just for setting the month? Or could I use it to create buttons for navigating to the month containing today, yesterday, and last/next week?
The SetMonth, SetNextMonth and SetPreviousMonth (to take the current naming) navigate to a given month.
They change the internal "visibleDate" state without impacting the value.
Which is why I'm trying to be carefull with the naming, to differentiate them from an actual Calendar.MonthButton that would be the month counterpart of Calendar.DayButton and that would update the value with a new month.
The SetMonth let you pass an explicit date and the component will just navigate to its month. You could create UIs that are not only arrow navigation with it.
For instance you could create a month view that list all the views of the current year and when you click on a month it navigates to it. But unlike the month view of MUI X, it wouldn't update the value, it would just navigate. On my PoC with month and year view, the month view was able to accept Calendar.MonthButton and Calendar.SetMonth so that people could either navigate or update the value depending on the UX they want.
An alternative DX would be to only have Calendar.MonthButon with a prop that determine if we update the month in the value (which will also automatically navigate to this month), or if we just update the visible date for navigation.
MUI has this calendar where it has a weird dropdown thing that opens the year view. Imo, this should just be a Select. Or perhaps two selects beside each other, one for month, one for year. I'm thinking it would just be straight Base UI Select, you just compose the primitives yourself. Would we need to expose a hook or something for this, to update the calendar via Select's onValueChange? (or however code works)
I tried to replicate the MD2 design here but I wouldn't say it's optimal clearly...
I need to do an experiment using a Select to see what we need to do to make it work. I wouldn't be surprised if it worked fine by just controling the value.
And to replicate the Material UX people could use <ListBox /> when available.
This is all getting complex quickly. So I'm going to put together a shaping doc in Notion over the weekend, where I'll collect research notes.
Good idea
Regarding what we have in this PR exactly, since there is quite a lot of internals to setup that make the PR very big, I'd be in favor of keeping the scope as small as possible even if it means having a calendar not ready for release when we merge. It would allow me to start working on the field sooner. That would of course mean keeping the doc pages private and not exporting the component.
Is that something you did in the past ? To have several PRs before the component is released ?
Alternatively I can merge into another git branch and then have one single PR that goes to master when the component is ready, but we would loose information in the Git history.
I meant 6โ10 July is missing from the June view.
For that one I kept the DX and default behavior from MUI X (which can be questioned for sure).
You have a prop fixedWeekNumber that allow you to fix the amount of visible weeks (6 being the number that will give you the exact same amount of week for every month of the gregorian calendar).
And by default we only take all the weeks that have at least one day in the visible month.
Behind the scene I'm using my new useWeekList hook that has a different DX, it takes an amount parameter that has the following API:
/**
* The amount of weeks to return.
* When equal to "end-of-month", the method will return all the weeks until the end of the month.
* When equal to a number, the method will return that many weeks.
* Put it to 6 to have a fixed number of weeks across months in Gregorian calendars.
*/
amount: number | 'end-of-month';
We could probably align both DX.
Note that useWeekList doesn't necessary start its list at the beginning of the month (that's needed for the scheduler), which is why I talk about "end-of-month" and not "full-month" or something like that.
Hey! are there any updates on this? :eyes: if its needed ill finish whatever fixes are left to close this branch.