mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.52k stars 1.31k forks source link

Replacement of the disableMaskedInput prop in DatePicker v6 #10734

Closed rettoua closed 1 year ago

rettoua commented 1 year ago

The problem in depth πŸ”

I'm upgrading x-date-pickers to the v6 version and I cannot find an alternative to the disableMaskedInput prop that was removed. I found slotProps.field and textField but they both don't have such or similar prop.

Your environment 🌎

`npx @mui/envinfo` System: OS: macOS 14.0 Binaries: Node: 18.18.0 - /usr/local/bin/node Yarn: 1.22.19 - /opt/homebrew/bin/yarn npm: 9.8.1 - /usr/local/bin/npm Browsers: Chrome: 118.0.5993.88 Edge: Not Found Safari: 17.0 npmPackages: @emotion/react: ~11.11.1 => 11.11.1 @emotion/styled: ~11.11.0 => 11.11.0 @mui/base: 5.0.0-beta.20 @mui/core-downloads-tracker: 5.14.14 @mui/icons-material: ~5.14.14 => 5.14.14 @mui/lab: ~5.0.0-alpha.149 => 5.0.0-alpha.149 @mui/material: ~5.14.14 => 5.14.14 @mui/private-theming: 5.14.14 @mui/styled-engine: 5.14.14 @mui/styles: ~5.14.14 => 5.14.14 @mui/system: ~5.14.14 => 5.14.14 @mui/types: 7.2.6 @mui/utils: 5.14.14 @mui/x-data-grid: 6.16.2 @mui/x-data-grid-premium: ~6.16.2 => 6.16.2 @mui/x-data-grid-pro: 6.16.2 @mui/x-date-pickers: ~6.16.2 => 6.16.2 @mui/x-license-pro: 6.10.2 @mui/x-tree-view: ~6.0.0-beta.0 => 6.0.0-beta.0 @types/react: ~18.2.21 => 18.2.21 react: ~17.0.2 => 17.0.2 react-dom: ~17.0.2 => 17.0.2 typescript: ~4.9.5 => 4.9.5

Search keywords: datepicker v6 Order ID: 54867

flaviendelangle commented 1 year ago

Hi,

What was your use-case for using disableMaskedInput?

rettoua commented 1 year ago

hi @flaviendelangle, I need a user to be able to type 1/1/2023 like a string input. Currently, you cannot select all - remove - type. Also, you need to use the keyboard navigation button to move to the next part of the date if the month or day is a single-digit number.

flaviendelangle commented 1 year ago

In this case, you should probably override slotProps.field and just render a TextField inside it. We consider that the smart editing is a lot easier to work with for most people than a free text.

Currently, you cannot select all - remove - type.

https://github.com/mui/mui-x/assets/3309670/6531e16c-4e6d-413f-8038-c9395556888a

Is this the behavior you are talking about, if not could you describe it in detail?

Also, you need to use the keyboard navigation button to move to the next part of the date if the month or day is a single-digit number.

Yes, just like on the Chrome native date input. We are discussing potential improvements in #9595

rettoua commented 1 year ago

Is this the behavior you are talking about, if not could you describe it in detail?

mask should be hidden and the user needs to type slash by themself. In general - it needs to work in the same way as it is in v5 version with disableMaskedInput

flaviendelangle commented 1 year ago

The v6 does not use any mask. If the UX provided by our new version does not suit you, you can either try to build a custom slots.field as proposed above or stick with v5. But I honestly don't think a free text input is a good UX to enter a date.

flaviendelangle commented 1 year ago

We can keep this issue with "Waiting for upvote" to propose a custom implementation in this doc section with a free text input

rettoua commented 1 year ago

... or stick with v5

there are changes we need from v6, we cannot stick with v5 forever since components evolving, new trends appear .etc.

But I honestly don't think a free text input is a good UX to enter a date

There are plenty of products that use such an approach, you can check Microsoft Business Central for inspiration.

you can either try to build a custom slots.field

is there an example anywhere? I don't see how to make it work, it crashes here and there....

flaviendelangle commented 1 year ago

is there an example anywhere? I don't see how to make it work, it crashes here and there....

On the page I linked you have examples of custom fields implementation. The one with the AutoComplete can help you, the main challenge is to wire the incoming props to keep all the behavior working.

There are plenty of products that use such an approach, you can check Microsoft Business Central for inspiration.

And there are plenty of products using other approaches (several selects for instance). Our component needs to stick with a coherent UX that we try to make as good as possible, but it be all those approaches at once. We are providing the tool to let people provide there own field if they want and we help them build it if we see that it's something several people are asking for. Concerning your specific needs, you are the 1st person to ask for it since the release of v6 (which covers about 50% of our users now), so I think not a lot of developers are trying to do it.

rettoua commented 1 year ago

And there are plenty of products using other approaches (several selects for instance).

Sure there are, I didn't say that what I need is the only correct approach. This is simply a requirement.

Our component needs to stick with a coherent UX that we try to make as good as possible, but it is all those approaches at once.

You cannot dramatically change behavior or discard capabilities with no mentions or at least there needs to be a backward compatibility to still be able to keep old behavior. Suggested for creating custom implementation - is not an approach, if I need something custom - I would do it by myself and won't use MUI. It second week is being passed with upgrading DataGrid and Pickers to the new versions, so many broken behaviours... Telerik, DevExpress - big vendors that you can learn from how to make releases. The current v5 -> v6 upgrade is total frustration; except for that - MUI is great πŸ‘ . I

flaviendelangle commented 1 year ago

You cannot dramatically change behavior or discard capabilities with no mentions or at least there needs to be a backward compatibility

The new UI is presented in the blog post that is linked in the migration guide.

if I need something custom - I would do it by myself and won't use MUI.

MUI provides components with a default behavior and gives you tools to customize it if the default behavior does not suits your needs. That's exactly what is being done here.

I'm going to close this issue because it's going nowhere.

rettoua commented 1 year ago

MUI provides components with a default behavior and gives you tools to customize it if the default behavior does not suits your needs.

Yeah, and this is the problem - changing default behavior, it is the way that will lead you nowhere...

flaviendelangle commented 1 year ago

We are changing the default behavior when we consider that the improvements in the default behavior outweigh the average cost of migration.

And given the feedback we had on this specific change, I am confident that it was the right decision. Even if for some people, unfortunately, the cost of migration is greater than the benefit brought by the change.

LukasTy commented 1 year ago

The current v5 -> v6 upgrade is total frustration; except for that - MUI is great πŸ‘ .

@rettoua We are sorry to hear that you are having problems with the migration. 😞 Could you expand on what were your biggest pain points with the migration? πŸ€”

rettoua commented 1 year ago

What happened with the v5 -> v6 upgrade: The project fell apart - after that fixing errors, finding ways/workarounds to restore previous behavior (this includes even looking at the mui source code - a good example is gridApi.getAllRowIds()). All this means that development has stopped, and it's not about one day... What do managers think when someone says "we cannot do anything since we upgrading MUI"? Nothing good actually, and I know that another upgrade will be sooner or later (what to expact from it?). Your current approach is to force clients to perform the upgrade in a one shot with postponing any other activities. How it could work to make clients happier than now? Make backward compatibility with old behavior - it's up to the vendor to decide how to do it (marking props, and functions with @depricate, providing alternative functions or so), it can be announced that with the next release all @deprecated capabilities will be removed. With that approach you can have a controlled upgrade when in the first step packages are updating with fixing breaking changes (that are not forced to rewriting half of functions). After that, all @deprecated capabilities can be tackled by applying new approaches one by one by keeping development alive with upgrading in parallel. DevExpress has done a good job here - they give (or gave, it was quite a time ago I was working with their components) you time to rewrite deprecated capabilities by keeping old behavior, till the next release, with introducing new ones). It's IMHO, I tested Telerik and devexpress and still chose MUI, cause you're doing great product.

LukasTy commented 1 year ago

Thank you for your feedback. We'll try our best to make future migrations as smooth as possible.

Usually, we strive to introduce changes gracefully if that doesn't incur a significant cost in time or complexity.

In the case of Pickers, v6 was basically a complete overhaul of the internals as well as quite a bit of API. Certain changes were basically impossible to deliver gracefully, without ruining the experience for everyone. Remember that we barely had a few stable v5 releases and then jumped to pushing for v6. At this point, the whole codebase is significantly more mature and should require less rewriting (big breaking changes).

You can rest assured that v7 will not have as many breaking changes. πŸ‘Œ cc @mui/xgrid could be an insightful read regarding DataGrid migration

danielerhabor commented 9 months ago

I thought I'd mention that I have a similar use case where I need to read user input as "F2024" or "F24" or some custom code (that internally means fall 2024). Then, when the user hits the tab, it autocorrects the proper date to be the end date of fall 2024 like 2024-12-31.

type State = {
  dateValue: Moment | null;
  inputValue: string;
  errorMessage: string | null;
};

type Action = {
  payload: Partial<State>;
};

const reducer = (state: State, action: Action) => {
  return {
    ...state,
    ...action.payload,
  };
};

export function useCustomDatePicker(
  isStartDate: boolean,
  initialDateValue: moment.Moment | null = null,
) {
  const [state, dispatch] = useReducer(reducer, {
    dateValue: initialDateValue,
    inputValue: initialDateValue?.format("YYYY-MM-DD") ?? "",
    errorMessage: null,
  });
  const isTabPressed = useRef(false);
  const { dateValue, inputValue, errorMessage } = state;

  const processInput = (inputValue: string, isStartDate: boolean) => {
    let parsedInputValue: string;
    let validDateValue: Moment | null;
    try {
      parsedInputValue = validateDateInput(inputValue, isStartDate);
      validDateValue =
        parsedInputValue.trim() !== ""
          ? moment(parsedInputValue, "YYYY-MM-DD", true)
          : null;
      // TODO: modify `validateStarteOrEndDate` and the entirety of the `useCustomDatePicker` hook
      // such that given a non-null `validDateValue`, the user of this hook can provide custom error messages
      // such as "End Date cannot be in the past" or "Start Date cannot be in the future" depending on the circumstance
      // and/or depending on the kind of datepicker "autocomplete" scheme (start or end) that is being used
      validDateValue && validateStartOrEndDate(validDateValue, isStartDate);
      dispatch({
        payload: {
          errorMessage: null,
          inputValue: parsedInputValue,
          dateValue: validDateValue,
        },
      });
    } catch (error) {
      dispatch({
        payload: {
          errorMessage: (error as Error).message,
          inputValue: "",
          dateValue: null,
        },
      });
    }
  };

  const handleDateChange = (newDateValue: Moment | null) => {
    dispatch({
      payload: {
        dateValue: newDateValue,
        inputValue: newDateValue?.format("YYYY-MM-DD") ?? "",
      },
    });
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    dispatch({
      payload: {
        inputValue: event.target.value,
      },
    });
  };

  const handleBlur = useCallback(
    (event: React.FocusEvent<HTMLInputElement>) => {
      // if the "Tab" key was pressed, then don't process the input value via the "blur" event
      if (isTabPressed.current) return;
      processInput(inputValue, isStartDate);
    },
    [inputValue, isStartDate],
  );

  const handleKeyUp = useCallback(
    (event: React.KeyboardEvent<HTMLInputElement>) => {
      if (!(event.key === "Enter" || event.key === "Tab")) {
        return;
      }
      // if the key pressed is "Enter" or "Tab", then parse the input value
      // and set the value of the date picker
      processInput(inputValue, isStartDate);
    },
    [inputValue, isStartDate],
  );

  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Tab") {
      // add this so the the "blur" event is not triggered when the "Tab" key is pressed
      isTabPressed.current = true;
    }
  };

  return {
    dateValue,
    onDateChange: handleDateChange,
    inputValue,
    onInputChange: handleInputChange,
    errorMessage,
    onInputBlur: handleBlur,
    onInputKeyUp: handleKeyUp,
    onInputKeyDown: handleKeyDown,
  } as UseCustomDatePickerReturn;
}

type UseCustomDatePickerReturn = {
  dateValue: Moment | null;
  onDateChange: (date: Moment | null) => void;
  inputValue: string;
  onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  errorMessage: string | null;
  onInputBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
  onInputKeyUp: (event: React.KeyboardEvent<HTMLInputElement>) => void;
  onInputKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
};

export type CustomDatePickerProps = {
  isStartDate: boolean;
  sx?: SxProps;
  defaultLabel?: string;
  disabled?: boolean;
} & UseCustomDatePickerReturn;

export function CustomDatePicker({
  isStartDate,
  dateValue,
  onDateChange,
  inputValue,
  onInputChange,
  onInputBlur,
  onInputKeyUp,
  onInputKeyDown,
  errorMessage,
  sx,
  defaultLabel,
  disabled,
}: CustomDatePickerProps): JSX.Element {
  const label = defaultLabel ?? (isStartDate ? "Start Date" : "End Date");
  return (
    <DatePicker
      disabled={disabled}
      label={label}
      onChange={onDateChange}
      //   renderInput={(params) => (
      //     <TextField
      //       {...params}
      //       inputProps={{ placeholder: "YYYY-MM-DD" }}
      //       variant="standard"
      //       onBlur={onInputBlur}
      //       onKeyUp={onInputKeyUp}
      //       onKeyDown={onInputKeyDown}
      //       value={inputValue}
      //       onChange={onInputChange}
      //       helperText={errorMessage}
      //       error={!!errorMessage}
      //       sx={{
      //         height: "64px",
      //         ...sx,
      //       }}
      //     />
      //   )}
      slotProps={{
        // textField: {
        //   inputProps: { placeholder: "YYYY-MM-DD" },
        //   variant: "standard",
        //   onBlur: onInputBlur,
        //   onKeyUp: onInputKeyUp,
        //   onKeyDown: onInputKeyDown,
        //   value: inputValue,
        //   onChange: onInputChange,
        //   helperText: errorMessage,
        //   error: !!errorMessage,
        //   sx: {
        //     height: "64px",
        //     ...sx,
        //   },
        // },
        field: (
          <TextField
            inputProps={{ placeholder: "YYYY-MM-DD" }}
            variant="standard"
            onBlur={onInputBlur}
            onKeyUp={onInputKeyUp}
            onKeyDown={onInputKeyDown}
            value={inputValue}
            onChange={onInputChange}
            helperText={errorMessage}
            error={!!errorMessage}
            sx={{
              height: "64px",
              ...sx,
            }}
          />
        ),
      }}
      format="YYYY-MM-DD"
      value={dateValue}
      disableFuture={isStartDate}
      disablePast={!isStartDate}
      //   disableMaskedInput
    />
  );
}

validateDateInput basically takes a string like "s2023" or "2023-01-23" and parses it to the appropriate date as string. If it's already a date as string it stays the same, if it is "garbage" moment will try to then convert and fail and we show a red error message of "Invalid {Start|End} date".

I am trying to see if I can upgrade the date picker to v6 from v5 but this custom date picker is a key portion of our functionality. The old way of renderInput was able to work but I could barely find a way to get this to work without first coming to this thread. I just keep getting errors upon errors and the mask is still there. It's not letting me type free text. Not sure if you. We've been using MUI for a while now but this date picker business is preventing me from making upgrades. Is there something I am doing wrong? Again, if it's not possible, I'll just keep it as v5 with the current renderInput.

LukasTy commented 9 months ago

Hello @danielerhabor, thank you for taking the time to migrate. πŸ™ That is a very interesting UX case you are trying to accomplish there. 🀯 But if that's some internal application or users are already very used to the said functionalityβ€”maybe it's worth keeping it. πŸ‘Œ Have you checked the Custom field documentation? You might be in particular interested in the example with Autocomplete or Button. The idea in both of them is to ditch the original DateField responsible for all the "mask" functionality with a simpler solution, which in your case can just be a TextField similar to how you've done it.

P.S. Remember the correct usage of slots. πŸ˜‰