-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
DateTimePicker: Replace react-dates and moment with useLilius and date-fns #43005
DateTimePicker: Replace react-dates and moment with useLilius and date-fns #43005
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, it's so nice to be getting rid of that keepFocusInside
hack. 🎉
useEffect( () => { | ||
if ( ref.current && isFocusable && isFocusAllowed ) { | ||
ref.current.focus(); | ||
} | ||
}, [ isFocusable ] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the alternative is to set focus in the onKeyDown
event handler directly. Is that something you considered? It'd mean there's no need for isFocusAllowed
.
I guess you might have to use a data
attribute or something to find the appropriate Day
element in the DOM that way, and the event handler would probably need to be bound to the parent month.
It works nicely as it is, so no need to change it, just curious.
Should isFocusAllowed
be in the deps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad you're here! This keyboard stuff took me a day to get right and the whole time I was thinking of you and your struggles with RovingTabIndex
😂
I guess the alternative is to set focus in the
onKeyDown
event handler directly. Is that something you considered? It'd mean there's no need forisFocusAllowed
.
Yeah, I did think about it, but it makes it difficult to implement the nice behaviour where if you arrow past the beginning/end of the grid it automatically switches to the previous/next month. You have to wait until the new month renders before calling focus()
since otherwise the element won't exist in the DOM yet. You get this for free when using this state approach since React will call useEffect()
after rendering the new state.
Should isFocusAllowed be in the deps?
If we did this then we'd be calling focus()
on an element that already has focus. I suppose it doesn't actually matter... idk, I like that the intention here is explicit which is that we only want to focus the element when isFocusable
changes to true
😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth adding a comment to the code to explain why it's missing from the deps. It's something a well meaning dev would change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I tried to explain it all using some comments, although it feels like I just wrote the word "focus" a dozen times in a row 😅
className="components-datetime__date__day" // Unused, for backwards compatibility. | ||
disabled={ isInvalid } | ||
tabIndex={ isFocusable ? 0 : -1 } | ||
aria-label={ getDayLabel( day, isSelected, numEvents ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getDayLabel
looks like a candidate for useMemo
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed with you but then I ended up having to remove the push
stuff from getDayLabel
because it didn't really work in RTL languages. This means getDayLabel
is now a simple if
and therefore I think not necessary to memoize.
(Could also React.memo
the whole of Day
if we wanted to. Idk. Probably it's premature optimisation...)
…e-fns Updates DateTimePicker, TimePicker and DatePicker to use date-fns and useLilius instead of react-dates and moment. Both libraries result in less minified bytes sent to the client. useLilius also has other advantages: less component remounting, can use dateI18n to localise labels, can use @wordpress/components components, can use Emotion instead of SCSS.
d30f194
to
1951ead
Compare
Size Change: -38.7 kB (-3%) Total Size: 1.24 MB
ℹ️ View Unchanged
|
role="application" | ||
aria-label={ __( 'Calendar' ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied this from the previous implementation but I'm not sure if role=application
is the right thing to do. Maybe someone who knows what they're doing can answer cc. @talldan @tellthemachines
I've been testing this using keyboard and mouse interactions in LTR and RTL langs and it's working great!
🎉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks for tackling this difficult task!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @noisysocks thank you for working on this!
Even though this PR has already been merged, I went ahead and gave it a good look.
Apart from my inline comments, here's some additional feedback:
-
we should add
box-sizing
reset styles to the root element of each component (DatePicker
,TimePicker
,DateTimePicker
) -
When the calendar is focused, pressing arrow keys scrolls the page (we should preventDefault or stopPropagation)
-
The "calendar help" text was not updated to remove support for page up / page down / home / end keys (unless we didn't mean to remove support for those keys)
-
The font size "day of the week" text (i.e "Wed", "Thu") should be set in a way that doesn't change when the wordpress Global Styles are added or removed (you can use the switch in Storybook to toggle them on/off)
-
Could we look into replacing
userEvent.setup( { delay: null } )
withconst user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } )
?
A couple of additional thoughts:
- The component's chosen timezone seem to be tightly coupled with the WordPress editor's settings — we should work on a way to decouple this, and maybe have the desired timezone offset as a prop?
- It would be great if the @wordpress/components
package didn't import any date library at all (i.e.
date-fns), and instead used
@wordpress/date` as its date utility library - Of course, we should look into migrating
@wordpress/date
away frommoment
too, but that's a whole different story
(cc'ing @sgomes since he was involved in previous conversations where we evaluated Temporal
).
@@ -47,7 +43,7 @@ describe( 'DatePicker', () => { | |||
); | |||
|
|||
await user.click( | |||
screen.getByRole( 'button', { name: 'Friday, May 20, 2022' } ) | |||
screen.getByRole( 'button', { name: 'May 20, 2022' } ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why we changed the button's accessible name (removing the week-day name) in this refactor, instead of making the new implementation match the old one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Using the format string in wp.date.__experimentalGetSettings().formats.date
instead of a hardcoded format string ensures that the ARIA label respects the site's Date Format setting:
It also ensures that the format string is internationalised because the format string obtained from wp-date
has been passed through __
:
This is important as not all languages use {Day}, {Month} {Day}, {Year}
.
Worth noting that prior to this PR these labels weren't working anyway. The labels contained format string tokens:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, thank you for the context!
"Are you free on Thursday F, j?"
day: format( date, 'dd' ), | ||
month: format( date, 'MM' ), | ||
year: format( date, 'yyyy' ), | ||
minutes: format( date, 'mm' ), | ||
hours: format( date, is12Hour ? 'hh' : 'HH' ), | ||
am: format( date, 'a' ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could these also be moved to the constants file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The format strings? I think it's nice having them near to where they're used as the reader has to jump around less.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, although I thought that would be more coherent with having the TIMEZONELESS_FORMAT
as a variable in a separate file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The difference there is that TIMEZONELESS_FORMAT
is used by two components (TimePicker
and DatePicker
) so makes sense to have it in a separate file where both can get at it.
}; | ||
|
||
const momentDate = getMomentDate( currentDate ); | ||
}, [ isFocusable ] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const { formats } = __experimentalGetSettings(); | ||
const localizedDate = dateI18n( | ||
formats.date, | ||
date, | ||
-date.getTimezoneOffset() | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously we were not relying on @wordpress/date
:
- could we use
date-fns
for formatting the date instead ? - alternatively, should we aim at using
@wordpress/date
more instead ofdate-fns
?
Ultimately, I'm trying to understand if we get to "one source" of date utils, instead of using both third party libraries AND internal libraries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use format
from date-fns
but we would need to set up internationalisation which is a little tricky to do in a way that doesn't increase the size of the build. We'd have to pass the locale to the component which I'm not a huge fan of because it makes it difficult to switch DatePicker
away from date-fns
in the future.
https://date-fns.org/v2.16.1/docs/I18n
dateI8n
from @wordpress/date
already handles localisation and is configured properly by WordPress which is why I'm using it here.
My view on @wordpress/date
is that it is complimentary to a date util library (e.g. date-fns
) and should only provide:
- A store for the WordPress site's date settings including format preference and timezone.
- A function similar to
wp_date
in PHP.
Everything else should be deprecated from @wordpress/date
in favour of date-fns
. Right now I believe that's just the isInTheFuture
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. Not something that needs to be addressed urgently, but I'll see if I can open an issue on next steps for @wordpress/date
(which should include what we discussed in this comment too)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Thanks so much @ciampo! I've opened #43495 to address your notes.
We are relying on the date format that you get from
I wrote my thoughts on this in #43005 (comment). In short I think both packages are complimentary.
Yes! |
What?
Follows #41385.
Updates
DatePicker
to useuseLilius
instead ofreact-dates
to render its calendar and removesmoment
as a dependency from@wordpress/components
in favour ofdate-fns
.https://github.com/its-danny/use-lilius
https://github.com/date-fns/date-fns
Why?
Problems with
react-dates
moment
which is quite large and no longer actively developed.useEffect
to set anaria-label
on the day button.key
when the date is changed. (This makes the whole thing feel really slow.)@wordpress/components
components. Many of the UI elements can't be swapped out with@wordpress/components
components. This results in a less consistent UI and more CSS shipped to the browser.Considering
react-calendar
I looked at using
react-calendar
in #41385. This is a smaller alternative toreact-dates
that usesdate-fns
.Pros:
date-fns
, allowing us to removemoment
as a dependency.Cons:
@wordpress/components
components. Many of the UI elements can't be swapped out with@wordpress/components
components. This results in a less consistent UI and more CSS shipped to the browser.Considering
useLilius
I then looked at
useLilius
per a suggestion by @stokesman. This is a "headless" library that usesdate-fns
. It offers a single hook that can be used to display a calendar and not any actual UI components.Pros:
date-fns
, allowing us to removemoment
as a dependency.@wordpress/components
components and Emotion for all of the UI.Cons:
Considering
Temporal
The
Temporal
API is now stage 3, so I considered a completely bespoke implementation usingTemporal
.Pros:
Temporal.PlainDate
is perfectly suited for implementing a calendar as it does not contain time or timezone information, both of which are not relevant in the context of choosing a date from a calendar.Temporal
will assumedly be a native browser API in due course.Cons:
Temporal
in production.Temporal
. What's more,Temporal
is much more difficult to tree shake thandate-fns
because all of the functions are implemented as methods on e.g.Temporal.PlainDate.prototype
. This means the polyfill we ship is quite large.Conclusion:
useLilius
+date-fns
I settled on using
useLilius
and rolling our own implementation of keyboard navigation. This approach offers the most upfront benefit and is relatively straightforward to implement.This approach doesn't preclude a later switch to
Temporal
. When theTemporal
proposal is more developed and implemented by at least one widely used browser (e.g. Chrome), it will be less likely that a user needs to download a heavy polyfill. We should then be able to easily implement our own version ofuseLilius
that usesTemporal.PlainDate
. If you look at the source ofuseLilius
you'll see that it's quite small.How?
@wordpress/components
: Removemoment
andreact-dates
dependencies. Adddate-fns
anduse-lilius
.DateTimePicker
: UseVStack
so that we can remove all CSS.TimePicker
: Swapmoment
functions fordate-fns
functions.DatePicker
useLilius
instead ofreact-dates
.@wordpress/components
and Emotion instead of CSS.dateI18n
so that dates are correctly translated.wp.date.getSettings()
so that dates appear in the selected format.Testing Instructions
Screenshots or screencast
Before:
before.mp4
After:
after.mp4
I don't know if it comes across on video but it feels much snappier to use because we're no longer remounting the component 🙂