Open flaviendelangle opened 4 days ago
Here is the structure that would be used to recreate the navigation of the MD3 Date Picker:
In that example, will Calendar.Header.Label
also render a dropdown for month selection? Or will there be another component for that?
Would *.*.Cell
components accept a render
prop for custom cells? Or should they be replaced by custom components?
In that example, will Calendar.Header.Label also render a dropdown for month selection? Or will there be another component for that?
<Calendar.Header.Label />
would only render the current label inside a div (span ?), with no behavior attaches.
It's mostly a shortcut to have a default label without introducing utility hooks here. Maybe this component is not even that useful, but I think it's a nice shortcut.
If you want to change month, then you can wrap it as follow:
<Calendar.Navigation.SetView target="month">
<Calendar.Header.Label format="MMMM" />
</Calendar.Navigation.SetView>
And then when the month view is handled by Calendar.Months.*
components.
Overall your code would look something like that (it just misses some way of hiding the views that are not the current views but I need to think about the DX here):
<Calendar.Root>
<Calendar.Header.Root>
<div>
<Calendar.Navigation.SetMonth target="previous" />
<Calendar.Navigation.SetView target="month">
<Calendar.Header.Label format="MMMM" />
</Calendar.Navigation.SetView>
<Calendar.Navigation.SetMonth target="next" />
</div>
<div>
<Calendar.Navigation.SetYear target="previous" />
<Calendar.Navigation.SetView target="year">
<Calendar.Header.Label format="YYYY" />
</Calendar.Navigation.SetView>
<Calendar.Navigation.SetYear target="next" />
</div>
</Calendar.Header.Root>
<Calendar.Days.DefaultLayout>
{value => <Calendar.Days.Cell value={value} />}
</Calendar.Days.DefaultLayout>
<Calendar.Months.Root>
{value => <Calendar.Months.Cell value={value} />}
</Calendar.Months.Root>
<Calendar.Years.Root>
{value => <Calendar.Years.Cell value={value} />}
</Calendar.Years.Root>
</Calendar.Root>
If you don't want a month view but instead you want to render some month picking UI inside a drop down in the header, you could do something like the following (I did not check the DX of the Base UI Popover in depth, it's just to get the idea):
<Popover.Root>
<Calendar.Header.Label format="MMMM" render={<Popover.Trigger />} />
<Popover.Popup>
<Calendar.Months.Root>
{month => <Calendar.Months.Cell value={month} />}
<Calendar.Months.Root>
</Popover.Popup>
</Popover.Root>
Since it's the DX of the unstyled component, I'm trying to be as little optionated as possible about the end UX.
Would ..Cell components accept a render prop for custom cells? Or should they be replaced by custom components?
For me any component built for composition in an unstyled package should follow some common DX and the render
prop is one of them (another one would be the className
prop with a string | (state) => string
signature).
So yes for me we would be able to do:
const MyCustomDay = styled('button')({}) // or any other way of creating and styling a component
<Calendar.Days.DefaultLayout>
{value => <Calendar.Days.Cell value={value} render={<MyCustomDay />} />}
</Calendar.Days.DefaultLayout>
but also:
const MyCustomDay = styled(Calendar.Days.Cell)({});
<Calendar.Days.DefaultLayout>
{value => <MyCustomDay />}
</Calendar.Days.DefaultLayout>
but also:
<Calendar.Days.DefaultLayout>
{value => <Calendar.Days.Cell className={state => clsx('day', state.selected && 'day-selected')} />}
</Calendar.Days.DefaultLayout>
Here I'm following 100% the Base UI DX (or at least what I understand of it, I might get some things wrong)
What's the purpose of Calendar.Days.DefaultLayout
?
What's the purpose of Calendar.Days.DefaultLayout?
React Aria supports two DX:
DX n°1:
<CalendarGrid>
{(date) => <CalendarCell date={date} />}
</CalendarGrid>
DX n°2:
<CalendarGrid>
<CalendarGridHeader>
{(day) => <CalendarHeaderCell />}
</CalendarGridHeader>
<CalendarGridBody>
{(date) => <CalendarCell date={date} />}
</CalendarGridBody>
</CalendarGrid>
I'm proposing to also have this shortcut notation (because it makes the composition easier to get started with). But I'm proposing to split the two DX into two distinct components to simplify the explanation for each one of those (that's clearly debatable and purely a product topic, on the engineering side both are easily doable).
So:
<Calendar.Days.DefaultLayout />
= <CalendarGrid />
in React Aria for DX n°1<Calendar.Days.Root />
= <CalendarGrid />
in React Aria for DX n°2 I guess we have three ways of doing it:
<Calendar.Days.Root />
just like React Aria does both on <CalendarGrid />
I like 3. because it could allow us to create some shared nomenclature for the few components that have a richer DOM structure (I'm thinking about the virtualization engine of the Tree View of perhaps the grid if we have composition someday).
That way people could learn that unless a component starts with Default*
(or another prefix), then they can safely assume that it only has one DOM element.
- We don't support DX n°1. Base UI is more strict than React Aria and always have 1 component = 1 DOM element.
Got it. I like this option.
With any more than 1 DOM element per component, we have the same customization issues that we are trying to solve with composition, right?
With any more than 1 DOM element per component, we have the same customization issues that we are trying to solve with composition, right?
Yes Which is why for me it's mandatory to have a DX with 1 component = 1 DOM element (which React Aria doesn't even have since you don't own the week DOM element :grimacing: ).
But the question here would more be: should we only provide the version where 1 component = 1 DOM element or are there use cases where providing both the 1 component = 1 DOM element and slightly higher level component make sense? React Aria sets classes on its elements (unlike Base UI) which makes the composition problem a lot less critical.
If we are not sure its bring value, we can definitely wait before adding it to see if there is traction.
Which is why for me it's mandatory to have a DX with 1 component = 1 DOM element (which React Aria doesn't even have since you don't own the week DOM element 😬 ).
Agree with this 👍 . And yeah, the React Aria approach is restrictive.
I'm strongly in favor of only supporting 1 component = 1 DOM for our composable components. It's less to maintain and less to document. And with less API to document, less confusion for users as they only have to learn one way to do things.
I'm strongly in favor of only supporting 1 component = 1 DOM for our composable components
I can remove Calendar.Days.DefaultLayout
since it's benefits are not clear.
But I think we will have use cases where we won't want to always have this 1 component = 1 DOM element, especially for the virtualization engine. But we'll see when we actually try to design a composable API for such use cases.
I'll update the document :+1:
I do quite like how the components are broken down into subcomponents e.g. Calendar.Days.Header
and Calendar.Header.Label
. It reads really nicely. I wonder if we need the third level of depth though, it could be:
<Calendar.Root>
<Calendar.Header>
<Calendar.SetMonth target="previous" />
<Calendar.HeaderLabel />
<Calendar.SetMonth target="next" />
</Calendar.Header>
<Calendar.Days>
<Calendar.DaysHeader>
{value => <Calendar.DaysLabel value={value} />}
</Calendar.DaysHeader>
<Calendar.DaysContent>
{weekValue => (
<Calendar.DaysWeekLine value={weekValue}>
{dayValue => <Calendar.DaysCell value={dayValue} />}
</Calendar.DaysWeekLine>
)}
</Calendar.DaysContent>
</Calendar.Days>
</Calendar.Root>
This would be more in line with how they name things on Base UI, e.g.
<NumberField.ScrubArea>
<NumberField.ScrubAreaCursor />
</NumberField.ScrubArea>
Granted, there are fewer parts to their components. Perhaps the third level works for components with more parts, but I would be curious to get their thoughts on this in particular.
On a similar note, does having a mega component mean that tree shaking won't work? In the context of data grid, if a user doesn't want a toolbar (composed with Grid.Toolbar.Root
etc), will this still be in their bundle? This might not be as relevant to the other X components.
We spoke about having the data grid components under separate imports to get around this. Something like:
import * as Toolbar from '@mui/x-data-grid/Toolbar';
alendar.Header.Label. It reads really nicely. I wonder if we need the third level of depth though, it could be:
It's one of the main DX question I'd like to answer.
My first draft looked a lot like yours <Calendar.DaysCell />
etc...
I'll gather the opinion of the rest of the team before settling on a solution here :+1:
We should definitely be consistent across our components. IMHO if the Calendar
don't use nested components, no component should do it in X, except perhaps Grid.***
if you go this way. So that's a discussion we should clearly have with all the X teams.
does having a mega component mean that tree shaking won't work?
I think it does tree shake. At least for Base UI from what I understood tree shaking of components should work. But tbh I did not test and I might have misunderstood. This is indeed a big reason to split or not those big components. If we have tree shaking, I'd like not to split them because I think the DX is better. But if we don't have tree shaking...
With that being said, for a component like Calendar
, you will very likely use 90%+ of the code in most apps.
This might differ for other pickers components, and definitely for the grid it's not the same...
Personally I think I would prefer to have a Toolbar
component on the grid instead of everything hosted in Grid.Toolbar
, but I'm not 100% sure...
We spoke about having the data grid components under separate imports to get around this. Something like:
Just in case, Base UI was using import * as Slider
and migrated away from it in favor of import { Slider }
.
I don't remember the exact reasons but I tried to follow their DX in my RFCs (this one has no import though).
I think it does tree shake.
It does tree shake if the package has "sideEffecs": false
and module
set in package.json
(which is the case for MUI X packages).
The thing is that tree shaking is only done for the production bundle. In dev mode, the whole package will be imported if you do this:
import { Calendar } from '@mui/x-date-pickers';
// or
import { Toolbar } from '@mui/x-data-grid';
This is the same problem that is described in https://mui.com/material-ui/guides/minimizing-bundle-size/#when-and-how-to-use-tree-shaking
At least for Base UI from what I understood tree shaking of components should work.
In Base UI each component has its own import path: https://base-ui.com/components/react-accordion
import { Accordion } from '@base-ui-components/react/accordion';
I believe this is done to solve the slow dev mode issue I mentioned above.
It does tree shake if the package has "sideEffecs": false and module set in package.json (which is the case for MUI X packages). The thing is that tree shaking is only done for the production bundle. In dev mode, the whole package will be imported if you do this:
For me we are more talking of being able to tree shake Calendar
itself if you don't use <Calendar.Months.Root />
for instance.
For the tree shaking of the root, I fully agree with everything you said :laughing:
Part of #15558
This component would be the Base-UI-DX component to edit day, month and year for a single date. It would the fundation for
<MonthCalendar />
,<YearCalendar />
and<DateCalendar />
on the MUI X version.Basic examples
Most basic example (only day view, simple header)
Basic example with the day, month and year view
I need to clarify how we handle the view switch.
Basic example of
Calendar.Header.*
Example of
Calendar.Header.*
with month and year navigationHere is the structure that would be used to recreate the navigation of the MD3 Date Picker:
Basic example of
Calendar.Days.*
Component list
Misc:
Calendar.*
<Calendar.Root />
offset Props:Value props:
value
,defaultValue
,onChange
,referenceDate
,timezone
View props:
view
,defaultView
,onViewChange
Validation props:
minDate
,maxDate
,shouldDisableDate
etc...Form props:
disabled
,readOnly
Focus props:
autoFocus
(focusedView
,openTo
andonFocusedViewChange
need to be clarified)fixedWeekNumber
Header:
Calendar.Header.*
<Calendar.Header.Root />
<Calendar.Header.Label />
Pops:
format
${utils.formats.month} ${utils.formats.year}
string
Day view:
Calendar.Days.*
<Calendar.Days.Root />
Props:
fixedWeekNumber
displayWeekNumber
default:
false
<Calendar.Days.Content />
<Calendar.Days.WeekLine />
Props:
value
{ date: PickerValidDate }
<Calendar.Days.Cell />
Props:
value
{ type: 'day', date: PickerValidDate } | { type: 'weekNumber' }
Month view:
Calendar.Months.*
<Calendar.Months.Root />
<Calendar.Months.Cell />
Props:
value
{ date: PickerValidDate }
Year view:
Calendar.Years.*
<Calendar.Years.Root />
<Calendar.Years.View />
Props:
value
{ date: PickerValidDate }
Navigation:
Calendar.Navigation.*
<Calendar.Navigation.SetMonth />
Props:
target
"previous" | "next"
<Calendar.Navigation.SetYear />
Props:
target
"previous" | "next"
<Calendar.Navigation.SetView />
Props:
target
"year" | "month" | "day"
Props that will remain in the MUI X layer
The following props won't be present in the Base UI X component but only on the MUI X one because they only apply the style:
monthsPerRow
yearsPerRow
disableHighlightToday
(<Calendar.Days.Cell />
will set adata-today
attribute on the right day, month and year)showDaysOutsideCurrentMonth
(<Calendar.Days.Cell />
will set adata-outside-current-month
attribute, name tdb on the right days)renderLoading
reduceAnimations
loading
To clarify
How to handle multiple month visible at once (React Aria spec)? It would be a great addition to the
Calendar
component, currently only<DateRangeCalendar />
supports multiple calendars. When implementing it, we should be careful with the navigation (see thepageBehavior
in React Aria)Should it be
Calendar.Days.*
orCalendar.DayView.*
? (same forMonths
andYears
). The problem withCalendar.DayView
is that when you have only the "day" view (which is a super common use case), the concept of "view" is just a distraction.Should the components be
<Calendar.Header.Label />
or<Calendar.HeaderLabel />
? (same for all the others). AFAIK Base UI avoids deep nesting like<Calendar.Header.Label />
, but maybe we are big enough to justify adding this level of depth.How do we handle keyboard navigation in
Calendar.Months.*
andCalendar.Years.*
without being optionated on the layout? People should be able to design a list (like in MD3) or a grid (like we currently have). Maybe we can keep themonthsPerRow
/yearsPerRow
props on the Base UI X version but use it only for keyboard navigation purpose.The focus part is very unclear for now (which props? which DX?)
Planned work
This is very early stage but here is a basic idea of what the migration could look like:
@mui/x-date-pickers/internals/base-ui/Calendar
(no public doc, only private one). Requires to have Base UI as a dep of our package so we need to wait for the stable release (or at the very least the beta)@mui/x-date-pickers/DateCalendar
@mui/x-date-pickers/DateCalendar
to use the new component@base-ui/x-date-pickers
package)Search keywords: