Skip to content
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

[pickers] Improve the DX of custom fields #14496

Open
flaviendelangle opened this issue Sep 5, 2024 · 3 comments
Open

[pickers] Improve the DX of custom fields #14496

flaviendelangle opened this issue Sep 5, 2024 · 3 comments
Assignees
Labels
component: pickers This is the name of the generic UI component, not the React module! customization: extend Logic customizability umbrella For grouping multiple issues to provide a holistic view

Comments

@flaviendelangle
Copy link
Member

flaviendelangle commented Sep 5, 2024

We currently have a several examples on how to create fields with custom UIs (using browser primitives, using Joy UI, ...) and we have 2 demos on how to create fields with custom behaviors (one with a Button, one with an Autocomplete).

The goal of this issue is to improve the 2nd type of demo and to create a few new ones.

Here are the planned work:

Enhancements

Missing doc sections

  • useValidation
  • useSplitFieldProps
  • useParsedFormat
  • usePickersContext

Recipes for custom behavior field

Search keywords:

@flaviendelangle flaviendelangle added umbrella For grouping multiple issues to provide a holistic view component: pickers This is the name of the generic UI component, not the React module! customization: extend Logic customizability labels Sep 5, 2024
@flaviendelangle flaviendelangle self-assigned this Sep 5, 2024
@flaviendelangle
Copy link
Member Author

flaviendelangle commented Sep 6, 2024

Exploration of a Base UI DX (for built in editing behavior)

⚠️ The following document is a WIP, the API described may not be implemented as currently presented.

⚠️ If you are looking on how to build a field with a custom UI but with the built-in editing behavior, please have a look at #14496 (comment)

Goals

The content below describes what the DX could look like in 12-18 months, once we won't be supporting @mui/material/TextField anymore.

  1. People should be able to use built-in fields with Material UI as one-liner: (import { DateField } from '@mui/x-date-pickers/DateField')
import { DateField } from '@mui/x-date-pickers/DateField'

return <DateField />
  1. People should be able to easily build fields with a custom UI while keeping the same editing behavior as the built-in fields and without bundling @mui/material or @mui/system

  2. People should not have to care about implementation details of the fields (which prop should be passed to which DOM element) when building fields with a custom UI

Problem of the current DX

Too many concepts

Once the @mui/material/TextField component will not be a valid DOM structure for useField, we will end up with an architecture way too complex for what we will really need.

The goal is basically to merge useField and PickersSectionList into a single concept (PickersField) and to make it as customizable as possible thanks to the Base UI X to allow people to easily build replacement to PickersTextField using their own design system.

image

Difficult to know where each element returned by useField should go

The useField hook (or the hooks wrapping it like useDateField) return a ton of elements that must be passed to various DOM elements. This makes the creation of a custom UI very tedious.

New concepts

Value manager

This PR extends the concept of value manager to replace all the logic inside the field hooks (useDateField, useTimeField, ...).

It will be the only generic passed the PickersField (instead of passing TDate, TValue, TSection, TInternalProps) which makes the typing a lot easier for external users.

We would have one value manager per type of picker (date, time, date range, ...)

Here are the properties of a value manager . All of them already exist:

  • legacyValueManager: used by the picker and the field to interact with the value. I would like to merge it wih fieldValueManager and either inline it in the new valueManager or store it in a key named differently.
  • fieldValueManager: used by the field to interact with the value
  • validator: used by the picker, the field and the views to validate a value (would still be exposed as standalone for useValidation)
  • valueType: used by the field to make sure the format is compatible with the current field
  • applyDefaultFieldInternalProps: used by the field to apply its default values (will replace hooks like useDefaultizedDateField)

New PickersField component

This new component would follow the DX of Base UI and would be the cornerstone to build a field that uses our editing behaviors. It would be used to create DateField, TimeField, SingleInputDateRangeField, etc... and would also be used for people that want to create a field with a custom UI while using our editing behaviors (see this demo to have the current DX).

In short, it would replace both PickersSectionList, useDateField (and equivalent) hook and help make PickersTextField logic-agnostic.

import { getDateValueManager } from '@mui/x-date-pickers/valueManagers'
// We need to decide where those Material UI-less components would live
import { PickersField } from '@mui/x-date-pickers/base/PickersField' 

const valueManager = getDateValueManager<Dayjs>();

// Misses some parts like the clear buttonfor now
function MyDateField(props) {
  return (
    <PickersField.Root valueManager={valueManager} {...props}>
      <PickersField.Content>
        {(section) => (
          <PickersField.Section section={section}>
            <PickersField.SectionSeparator position="before" />
            <PickersField.SectionContent />
            <PickersField.SectionSeparator position="after" />
          </PickersField.Section>
        )}
      </PickersField.Content>
    </PickersField.Root>
  )
}

// The picker itself could have a composition version one day, but this is not in the scope of this issue
function MyDatePicker(props: DatePickerProps<Dayjs>) {
  return <DatePicker slots={{ field: MyDateField }} />
}

function MyApp() {
  return (
    <React.Fragment>
      <MyDatePicker value={value} onChange={setValue} />
      <MyDateField value={value} onChange={setValue} />
    </React.Fragment>
  )
}

Potential roadmap

This migration will be a very long one given that we will still support @mui/material/TextField in v8 and that Base UI is still far from being stable.
Thus I would love us to take the time to make this new long-term DX as good as possible instead of monkey-patching the logic.

Here is what a potential roadmap could look like:

  • v8.0.0 alpha (end of 2024)

    • BC: Make the new DOM structure the default (it will still use PickersSectionList under the hood) [fields] Enable the new field DOM structure by default #14651
    • Introduce the new value managers (internally) and use them in the field hooks (useDateField, useTimeField, ...)
    • Migrate our field components to use the value managers directly instead of the field hooks
    • Deprecate the field hooks
  • v8.X.X (mid 2025 ?)

    • Release the new value managers as stable
    • Release the PickersField component as unstable (@base_ui/react needs to be stable and battle tested enough so that MUI X accepts to start using it's utils)
    • Add doc examples that do not use @mui/material and that are built on top of PickersField to battle test the component (one using Base UI primitives, one using a third party design system like Shadcn)
  • v9.0.0 alpha (end of 2025)

    • Make PickersField stable
    • BC: Refactor PickersTextField to use PickersField under the hood (and maybe move it to a Material-specific endpoint)
    • Refactor the field component (DateField, TimeField, ...) to just be wrapper on top of the new version of PickersTextField (and maybe move them to a Material-specific endpoint)
    • BC: Remove the deprecated field hooks ( useDateField / useTimeField, ...)
  • v9.X.X (start of 2026)

    • Remove the internal useField hook and instead move its logic inside PickersField (with the Base UI DX, we no longer need to have a single hook that returns all the attributes passed to each DOM element, components like PickersField.Section should be able to handle some logic themselves).

@flaviendelangle
Copy link
Member Author

flaviendelangle commented Sep 13, 2024

Exploration of the hook API (for custom editing behavior)

⚠️ The following document is a WIP, the API described may not be implemented as currently presented.

⚠️ If you are looking on how to build a field with a custom UI but with the built-in editing behavior, please have a look at #14496 (comment)

Utilities

useSplitFieldProps

Split the props received by the field component into:

  • internalProps which are used by the various hooks called by the field component.
  • forwardedProps which are passed to the underlying component.
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');

usePickersFieldContext

Returns the context passed to the field by the surrounding picker.
For now it only contains onOpen (added in #14606), but it should contain more information in the future.

const pickerContext = usePickersFieldContext();

useValidation

Handles the validation of the current value.

const { 
  // The validation error associated to the value passed to the `useValidation` hook.
  validationError, 
   // `true` if the current error is not null.
   // For single value components, it means that the value is invalid.
   // For range components, it means that either start or end value is invalid.
  hasValidationError, 
  // Get the validation error for a new value.
  // This can be used to validate the value in a change handler before updating the state.
  // e.g: `const error = getValidationErrorForNewValue(dayjs())`
  getValidationErrorForNewValue 
} = useValidation({
  validator: validateDate,
  value,
  timezone,
  props: internalProps,
});

useFieldPlaceholder

Generates the placeholder associated with the current value.

const placeholder = useFieldPlaceholder(internalProps);

Typing

DatePickerFieldProps, TimePickerFieldProps, ...

Each picker will expose a XXXPickerFieldProps interface that will take 2 generics:

  • TDate (required)
  • TEnableAccessibleFieldDOMStructure (defaultized to the current default value in `useField)

This interface should describe as accurately as possible the props that this picker is passing to its field slot.

Custom slots should then be able to directly use this interface as their props:

import { DatePickerFieldProps } from '@mui/x-date-pickers/DateTimePicker';

const MyCustomDateTimeField = (props: DatePickerFieldProps<Dayjs>) => { ... }

Very basic example (read-only textfield)

import { Dayjs } from 'dayjs';
import TextField from '@mui/material/TextField';
import { DatePicker, DatePickerFieldProps } from '@mui/x-date-pickers/DatePicker';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
import { useSplitFieldProps, useFieldPlaceholder } from '@mui/x-date-pickers/hooks';

function ReadonlyDateField(props: DatePickerFieldProps<Dayjs>) {
  const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
  const { value, timezone, format } = internalProps;

  const pickerContext = usePickersFieldContext();
  const placeholder = useFieldPlaceholder(internalProps);
  const { hasValidationError } = useValidation({
    validator: validateDate,
    value,
    timezone,
    props: internalProps,
  });

  return (
    <TextField
      {...forwardedProps}
      value={value == null ? '' : value.format(format)}
      placeholder={placeholder}
      // TODO: Once the pickers no longer return the `InputProps`, 
      // we will also have to add the icon manually here.
      // And we should migrate to slots in the doc when we can, to be future proof.
      InputProps={{ ...forwardedProps.InputProps, readOnly: true }}
      error={hasValidationError}
      onClick={pickerContext.onOpen}
    />
  );
}

function ReadonlyFieldDatePicker(props) {
  return (
    <DatePicker slots={{ ...props.slots, field: ReadonlyDateField }} {...props} />
  );
}

@flaviendelangle flaviendelangle changed the title [pickers] Improve the DX of custom field with custom editing behaviors [pickers] Improve the DX of custom fields Sep 23, 2024
@flaviendelangle
Copy link
Member Author

flaviendelangle commented Sep 24, 2024

Exploration of a better doc structure

WIP

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: pickers This is the name of the generic UI component, not the React module! customization: extend Logic customizability umbrella For grouping multiple issues to provide a holistic view
Projects
None yet
Development

No branches or pull requests

1 participant