mui-x icon indicating copy to clipboard operation
mui-x copied to clipboard

[POC] New component DateField

Open flaviendelangle opened this issue 2 years ago • 10 comments

Doc Preview

Implement the single input approach described in https://github.com/mui/mui-x/issues/5531

Following our meeting on Tuesday 14th of July, I am starting a POC for a new date field. This input could become the default input in the DatePicker components at some point.

Wanted interactions

  • Ability to add a day / a month / a year with the "ArrowUp" and "ArrowDown" keys
  • Ability to switch from day to month to year with the "ArrowLeft" and "ArrowRight" keys

Missing @date-io methods

  • addYear (similar to addDay and addMonth) => https://github.com/dmtrKovalenko/date-io/pull/623

flaviendelangle avatar Jul 15 '22 10:07 flaviendelangle

These are the results for the performance tests:

Test case Unit Min Max Median Mean σ
Filter 100k rows ms 447.3 795.7 650.5 607.92 137.418
Sort 100k rows ms 570.6 1,096.1 570.6 909.1 191.507
Select 100k rows ms 213.9 299.9 278 262.12 32.464
Deselect 100k rows ms 139.1 306.3 202.1 228 66.256

Generated by :no_entry_sign: dangerJS against 842cf5ec3cfe50f9f54b4e4930b2f8078df79fd2

mui-bot avatar Jul 15 '22 10:07 mui-bot

The interactions with arrow keys feel great! Could it somehow block the implementation for a chain of digits as input?

joserodolfofreitas avatar Jul 15 '22 12:07 joserodolfofreitas

Btw, where does this POC leave us in terms of a single input implementation for a date range? Would a single input for a range use the same notion used here to isolate the specific 'pieces' of a date?

joserodolfofreitas avatar Jul 15 '22 12:07 joserodolfofreitas

Btw, where does this POC leave us in terms of a single input implementation for a date range?

I did not look into it yet It will definitely make the analysis of the input harder but maybe that's doable. Let's try to see if it's viable for a the simple use case and then stress test it on harder one.

We also have the date time where we may want to handle it like Retool and open a select, in which case the input must only cover the date part.

flaviendelangle avatar Jul 15 '22 12:07 flaviendelangle

Could it somehow block the implementation for a chain of digits as input?

I did not understand sorry

flaviendelangle avatar Jul 15 '22 12:07 flaviendelangle

Could it somehow block the implementation for a chain of digits as input?

I did not understand sorry

I raised the question because I noticed you cannot input digits on the current text field POC. It currently only supports the arrow keys, so the question is: Will there be any problem with supporting numeric values as well?

And by "chain of digits", I meant if the user could type numbers like: 1 5 0 7 2 0 2 2 to have an output like 15/07/2022

edit: Just saw the latest update to support numeric values, so the question is now partially answered: we can already input numbers!

joserodolfofreitas avatar Jul 15 '22 19:07 joserodolfofreitas

Will there be any problem with supporting numeric values as well?

Yes, I started to work on it. It raised a certain amount of questions so I'm trying to get a first working draft to discuss trade-offs. For instance on Telerik, when you have a full letter month, you can still type it's number and it sets the month (type 5 => sets "May")

flaviendelangle avatar Jul 16 '22 12:07 flaviendelangle

@joserodolfofreitas Concerning the editing, Telerik does not goes from what section of the date to another automatically.

If you are on the day and type "1", "2", "3", it will set the day to "1", then "12", then "3". To go to the section section you have to use the keyboard arrows.

Spectrum does move automatically to the next section. For numerical value it is doable. But I'm curious to see how they handle that for full letter months.


This behavior could be customizable if we have users asking for both

flaviendelangle avatar Jul 18 '22 11:07 flaviendelangle

This pull request has conflicts, please resolve those before we can evaluate the pull request.

github-actions[bot] avatar Jul 21 '22 09:07 github-actions[bot]

https://user-images.githubusercontent.com/3309670/183668935-f57ded52-f24e-4da8-93fc-fb01b466f916.mp4

I'm playing with the range version It add quite a lot of complexity, but it's totally doable with the Telerik approach

flaviendelangle avatar Aug 09 '22 14:08 flaviendelangle

This pull request has conflicts, please resolve those before we can evaluate the pull request.

github-actions[bot] avatar Aug 11 '22 15:08 github-actions[bot]

@flaviendelangle There seems to be an issue with preceding zeroes in days section.

https://user-images.githubusercontent.com/4941090/184370612-10f6e2d9-3e07-4d69-9d15-5b0c67bee71c.mov

Or am I missing something?

LukasTy avatar Aug 12 '22 14:08 LukasTy

@LukasTy there are probably quite a lot of similar problems yes If you want to have a look feel free :+1:

flaviendelangle avatar Aug 12 '22 14:08 flaviendelangle

Is the support for mobile part in the scope of this initial PR? I'm asking to know if I should leave feedback about it or not. I didn't see mentions about it.

oliviertassinari avatar Aug 12 '22 18:08 oliviertassinari

@oliviertassinari the scop is to identify early blockers that could force us to move for another strategy.

On my mobile device, the behavior is completely broken. I do not manage to enter a date

alexfauquette avatar Aug 12 '22 20:08 alexfauquette

This pull request has conflicts, please resolve those before we can evaluate the pull request.

github-actions[bot] avatar Aug 16 '22 00:08 github-actions[bot]

I'm curious for the day field in this preview — month seems to truncate to 2 digits and year to 4, but day seems to allow you minimum 3 digits with no maximum?

Also noting a pattern I've seen of the placeholder labels giving an indication for how many digits the input format is looking for: MM/DD/YYYY or DD/MM/YY

gerdadesign avatar Aug 16 '22 16:08 gerdadesign

The day bug should be fixed. The problem being that with invalid date (partially entered) the maximum value for day was not defined

alexfauquette avatar Aug 19 '22 15:08 alexfauquette

The focus management needs looking into.

  • Focus the field
  • change day with arrows
  • press ArrowRight -> focus jumps to year (and in case of date range field to the end range year)

LukasTy avatar Aug 21 '22 09:08 LukasTy

Regarding accessibility, this, to me, seems like an implementation very similar to the follow spin button example. I'd say we could implement the same a11y behaviour (of course with a following PR), unless you see that there would be any issues with it?

LukasTy avatar Aug 21 '22 09:08 LukasTy

Arrow (Up & Down) navigation for month field in letters only works after any month value has already been initialised. Clicking ArrowDown when January is selected and date is invalid (i.e. year is not selected) results in month field being reset to empty.

https://user-images.githubusercontent.com/4941090/185786358-ba57c0cc-32e1-409b-95da-d40de67e7a1a.mov

LukasTy avatar Aug 21 '22 09:08 LukasTy

Clearable example does not work correctly. It does not reset the value in the component.

LukasTy avatar Aug 21 '22 21:08 LukasTy

Time format/input does not seem like it wants to cooperate. 🤔

https://user-images.githubusercontent.com/4941090/185811088-f350b506-824e-4115-8a8a-93df947d0235.mov

LukasTy avatar Aug 21 '22 21:08 LukasTy

@LukasTy I rewrote the arrow up / down edition on invalid dates, it should be a lot better now

flaviendelangle avatar Aug 22 '22 11:08 flaviendelangle

@gerdadesign

Also noting a pattern I've seen of the placeholder labels giving an indication for how many digits the input format is looking for: MM/DD/YYYY or DD/MM/YY

For now the placeholder of an empty section is the name of the section (month for a month section for instance) Which is really basic and do not support any kind of localization. We should probably improve this, I'm open to suggestions for the default behavior and the customization options.

flaviendelangle avatar Aug 22 '22 11:08 flaviendelangle

Clearable example does not work correctly. It does not reset the value in the component.

@LukasTy I had a dumb bug where I did not use the controlled value in the component, several of your bugs were probably cause by this error, it is fixed now

flaviendelangle avatar Aug 22 '22 11:08 flaviendelangle

Clearable example does not work correctly. It does not reset the value in the component.

I had a dumb bug where I did not use the controlled value in the component, several of your bugs were probably cause by this error, it is fixed now

I can confirm that all my mentioned issues have been resolved. 🙌 👍

LukasTy avatar Aug 22 '22 14:08 LukasTy

@flaviendelangle What do you think about adding the following diff? It adds management of Home, End, PageUp and PageDown keys mentioned here as well as some function name alignment. Note: We currently don't have a clear way to know actual max or min year, hence I did not that Home/End management for it.

diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts
index 5470f19c..84c01798 100644
--- a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts
+++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts
@@ -11,12 +11,13 @@ import {
   UseFieldState,
 } from './useField.interfaces';
 import {
+  AvailableAdjustKeyCode,
   cleanTrailingZeroInNumericSectionValue,
   getMonthsMatchingQuery,
   getSectionValueNumericBoundaries,
   getSectionVisibleValue,
-  incrementDateSectionValue,
-  incrementOrDecrementInvalidDateSection,
+  adjustDateSectionValue,
+  adjustInvalidDateSectionValue,
   setSectionValue,
 } from './useField.utils';
 
@@ -153,7 +154,7 @@ export const useField = <
       }
 
       // Reset the value of the selected section
-      case event.key === 'Backspace': {
+      case event.key === 'Backspace' || event.key === 'Delete': {
         event.preventDefault();
 
         if (readOnly) {
@@ -181,7 +182,12 @@ export const useField = <
       }
 
       // Increment / decrement the selected section value
-      case event.key === 'ArrowUp' || event.key === 'ArrowDown': {
+      case event.key === 'ArrowUp' ||
+        event.key === 'ArrowDown' ||
+        event.key === 'Home' ||
+        event.key === 'End' ||
+        event.key === 'PageUp' ||
+        event.key === 'PageDown': {
         event.preventDefault();
 
         if (readOnly || state.selectedSectionIndexes == null) {
@@ -196,21 +202,21 @@ export const useField = <
 
         // The date is not valid, we have to increment the section value rather than the date
         if (!utils.isValid(activeDate.value)) {
-          const newSectionValue = incrementOrDecrementInvalidDateSection(
+          const newSectionValue = adjustInvalidDateSectionValue(
             utils,
             activeSection,
-            event.key === 'ArrowUp' ? 'increment' : 'decrement',
+            event.key as AvailableAdjustKeyCode,
           );
 
           updateSections(
             setSectionValue(state.sections, state.selectedSectionIndexes.start, newSectionValue),
           );
         } else {
-          const newDate = incrementDateSectionValue(
+          const newDate = adjustDateSectionValue(
             utils,
             activeDate.value,
             activeSection.dateSectionName,
-            event.key === 'ArrowDown' ? -1 : 1,
+            event.key as AvailableAdjustKeyCode,
           );
 
           const newValue = activeDate.update(newDate);
diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts
index 0dcd3f56..64977a0d 100644
--- a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts
+++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts
@@ -37,33 +37,89 @@ export const getDateSectionNameFromFormat = (format: string): DateSectionName =>
   throw new Error(`getDatePartNameFromFormat don't understand the format ${format}`);
 };
 
-export const incrementDateSectionValue = <TDate>(
+export type AvailableAdjustKeyCode =
+  | 'ArrowUp'
+  | 'ArrowDown'
+  | 'PageUp'
+  | 'PageDown'
+  | 'Home'
+  | 'End';
+
+const getDeltaFromKeyCode = (keyCode: Omit<AvailableAdjustKeyCode, 'Home' | 'End'>) => {
+  switch (keyCode) {
+    case 'ArrowUp':
+      return 1;
+    case 'ArrowDown':
+      return -1;
+    case 'PageUp':
+      return 5;
+    case 'PageDown':
+      return -5;
+    default:
+      return 0;
+  }
+};
+
+export const adjustDateSectionValue = <TDate>(
   utils: MuiPickersAdapter<TDate>,
   date: TDate,
   datePartName: DateSectionName,
-  datePartValue: 1 | -1,
+  keyCode: AvailableAdjustKeyCode,
 ) => {
+  const delta = getDeltaFromKeyCode(keyCode);
+  const isStart = keyCode === 'Home';
+  const isEnd = keyCode === 'End';
   switch (datePartName) {
     case 'day': {
-      return utils.addDays(date, datePartValue);
+      if (isStart) {
+        return utils.startOfMonth(date);
+      }
+      if (isEnd) {
+        return utils.endOfMonth(date);
+      }
+      return utils.addDays(date, delta);
     }
     case 'month': {
-      return utils.addMonths(date, datePartValue);
+      if (isStart) {
+        return utils.startOfYear(date);
+      }
+      if (isEnd) {
+        return utils.endOfYear(date);
+      }
+      return utils.addMonths(date, delta);
     }
     case 'year': {
-      return utils.addYears(date, datePartValue);
+      return utils.addYears(date, delta);
     }
     case 'am-pm': {
-      return utils.addHours(date, datePartValue * 12);
+      return utils.addHours(date, delta ? 1 : 1 * 12);
     }
     case 'hour': {
-      return utils.addHours(date, datePartValue);
+      if (isStart) {
+        return utils.startOfDay(date);
+      }
+      if (isEnd) {
+        return utils.endOfDay(date);
+      }
+      return utils.addHours(date, delta);
     }
     case 'minute': {
-      return utils.addMinutes(date, datePartValue);
+      if (isStart) {
+        return utils.setMinutes(date, 0);
+      }
+      if (isEnd) {
+        return utils.setMinutes(date, 59);
+      }
+      return utils.addMinutes(date, delta);
     }
     case 'second': {
-      return utils.addSeconds(date, datePartValue);
+      if (isStart) {
+        return utils.setSeconds(date, 0);
+      }
+      if (isEnd) {
+        return utils.setSeconds(date, 59);
+      }
+      return utils.addSeconds(date, delta);
     }
     default: {
       return date;
@@ -277,13 +333,16 @@ export const getSectionValueNumericBoundaries = <TDate>(
   }
 };
 
-export const incrementOrDecrementInvalidDateSection = <TDate, TSection extends FieldSection>(
+export const adjustInvalidDateSectionValue = <TDate, TSection extends FieldSection>(
   utils: MuiPickersAdapter<TDate>,
   section: TSection,
-  type: 'increment' | 'decrement',
+  keyCode: AvailableAdjustKeyCode,
 ) => {
   const today = utils.date()!;
-  const delta = type === 'increment' ? 1 : -1;
+  const delta = getDeltaFromKeyCode(keyCode);
+  const isStart = keyCode === 'Home';
+  const isEnd = keyCode === 'End';
+  const shouldSetAbsolute = section.value === '' || isStart || isEnd;
 
   switch (section.dateSectionName) {
     case 'year': {
@@ -299,8 +358,8 @@ export const incrementOrDecrementInvalidDateSection = <TDate, TSection extends F
 
     case 'month': {
       let newDate: TDate;
-      if (section.value === '') {
-        if (type === 'increment') {
+      if (shouldSetAbsolute) {
+        if (delta > 0 || isEnd) {
           newDate = utils.endOfYear(today);
         } else {
           newDate = utils.startOfYear(today);
@@ -314,8 +373,8 @@ export const incrementOrDecrementInvalidDateSection = <TDate, TSection extends F
 
     case 'day': {
       let newDate: TDate;
-      if (section.value === '') {
-        if (type === 'increment') {
+      if (shouldSetAbsolute) {
+        if (delta > 0 || isEnd) {
           newDate = utils.endOfMonth(today);
         } else {
           newDate = utils.startOfMonth(today);
@@ -332,7 +391,7 @@ export const incrementOrDecrementInvalidDateSection = <TDate, TSection extends F
       const pm = utils.formatByString(utils.endOfDay(today), section.formatValue);
 
       if (section.value === '') {
-        if (type === 'increment') {
+        if (delta > 0 || isEnd) {
           return pm;
         }
         return am;
@@ -347,8 +406,8 @@ export const incrementOrDecrementInvalidDateSection = <TDate, TSection extends F
 
     case 'hour': {
       let newDate: TDate;
-      if (section.value === '') {
-        if (type === 'increment') {
+      if (shouldSetAbsolute) {
+        if (delta > 0 || isEnd) {
           newDate = utils.endOfDay(today);
         } else {
           newDate = utils.startOfDay(today);
@@ -362,9 +421,9 @@ export const incrementOrDecrementInvalidDateSection = <TDate, TSection extends F
 
     case 'minute': {
       let newDate: TDate;
-      if (section.value === '') {
+      if (shouldSetAbsolute) {
         // TODO: Add startOfHour and endOfHours to adapters to avoid hard-coding those values
-        const newNumericValue = type === 'increment' ? 59 : 0;
+        const newNumericValue = delta > 0 || isEnd ? 59 : 0;
         newDate = utils.setMinutes(today, newNumericValue);
       } else {
         newDate = utils.addMinutes(utils.setMinutes(today, Number(section.value)), delta);
@@ -375,9 +434,9 @@ export const incrementOrDecrementInvalidDateSection = <TDate, TSection extends F
 
     case 'second': {
       let newDate: TDate;
-      if (section.value === '') {
+      if (shouldSetAbsolute) {
         // TODO: Add startOfMinute and endOfMinute to adapters to avoid hard-coding those values
-        const newNumericValue = type === 'increment' ? 59 : 0;
+        const newNumericValue = delta > 0 || isEnd ? 59 : 0;
         newDate = utils.setSeconds(today, newNumericValue);
       } else {
         newDate = utils.addSeconds(utils.setSeconds(today, Number(section.value)), delta);

LukasTy avatar Aug 22 '22 14:08 LukasTy

I applied @LukasTy suggestion to handle other keys edition

flaviendelangle avatar Aug 23 '22 13:08 flaviendelangle

This pull request has conflicts, please resolve those before we can evaluate the pull request.

github-actions[bot] avatar Aug 23 '22 17:08 github-actions[bot]