mui-x
mui-x copied to clipboard
[POC] New component DateField
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 toaddDay
andaddMonth
) => https://github.com/dmtrKovalenko/date-io/pull/623
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
The interactions with arrow keys feel great! Could it somehow block the implementation for a chain of digits as input?
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?
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.
Could it somehow block the implementation for a chain of digits as input?
I did not understand sorry
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!
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")
@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
This pull request has conflicts, please resolve those before we can evaluate the pull request.
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
This pull request has conflicts, please resolve those before we can evaluate the pull request.
@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 there are probably quite a lot of similar problems yes If you want to have a look feel free :+1:
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 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
This pull request has conflicts, please resolve those before we can evaluate the pull request.
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
The day bug should be fixed. The problem being that with invalid date (partially entered) the maximum value for day was not defined
The focus management needs looking into.
- Focus the field
- change day with arrows
- press
ArrowRight
-> focus jumps toyear
(and in case of date range field to the end range year)
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?
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
Clearable example does not work correctly. It does not reset the value in the component.
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 I rewrote the arrow up / down edition on invalid dates, it should be a lot better now
@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.
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
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. 🙌 👍
@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);
I applied @LukasTy suggestion to handle other keys edition
This pull request has conflicts, please resolve those before we can evaluate the pull request.