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: logic 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: logic 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 (<PickerField />) 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

Manager

See #15395

New <PickerField /> 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 { useDateManager } from '@base-ui/x-date-pickers/managers'
import { PickerField } from '@base-ui/x-date-pickers/PickerField' 

// Misses some parts like the clear button for now
function MyDateField(props) {
const manager = useDateManager();

  return (
    <PickerField.Root manager={manager} {...props}>
      <PickerField.Content>
        {(section) => (
          <PickerField.Section section={section}>
            <PickerField.SectionSeparator position="before" />
            <PickerField.SectionContent />
            <PickerField.SectionSeparator position="after" />
          </PickerField.Section>
        )}
      </PickerField.Content>
    </PickerField.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 managers (internally) and use them in the field hooks (useDateField, useTimeField, ...)
    • Migrate our field components to use the managers directly instead of the field hooks
  • v8.X.X (mid 2025 ?)

    • Release the new managers as stable
    • Release the <PickerField /> 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)
    • Deprecate the field hooks and push people toward the new <PickerField /> component instead
    • Add doc examples that do not use @mui/material and that are built on top of <PickerField /> 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 <PickerField /> stable
    • BC: Refactor PickersTextField to use <PickerField /> 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 <PickerField /> (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 <PickerField.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)

⚠️ 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');

usePickerContext

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 = usePickerContext();

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 = usePickerContext();
  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: logic Logic customizability umbrella For grouping multiple issues to provide a holistic view
Projects
None yet
Development

No branches or pull requests

1 participant