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 pickers #14718

Open
flaviendelangle opened this issue Sep 24, 2024 · 0 comments
Open

[pickers] Improve the DX of custom pickers #14718

flaviendelangle opened this issue Sep 24, 2024 · 0 comments
Assignees
Labels
component: pickers This is the name of the generic UI component, not the React module! customization: logic Logic customizability design: base-ui It's about the headless components.

Comments

@flaviendelangle
Copy link
Member

flaviendelangle commented Sep 24, 2024

WIP

This is very early stage and will probably change dramatically before any actual development can happen.
If the team wants to discuss some API, I would prefer to focus on #14496 for now.

Introduction

What is the Base UI DX?

In this document, I talk a lot about the "Base UI DX". I use generic term to describe some of the key concepts that Base UI decided to follow in its components. Following those principles requires to use some Base UI utils but it does not mean using the Base UI components directly (which is an entire different topic).
Here is what using a Base UI component can look like:

<Popover.Root>
  <Popover.Trigger>Trigger</Popover.Trigger>
  <Popover.Positioner sideOffset={8}>
    <Popover.Popup>
      <Popover.Title>Popover Title</Popover.Title>
      <Popover.Description>Popover Description</Popover.Description>
      <Popover.Arrow />
    </Popover.Popup>
  </Popover.Positioner>
</Popover.Root>

Opinions

  • The Base UI DX will be highly valuable for some parts of the pickers.

  • Migrating the whole codebase to use the Base UI DX is a huge task that is not cost-effective in the forseable future, we should concentrate our efforts where it brings the most value to our users

  • The Base UI DX makes very little sense on some heavy UI-oriented like the Android analog clock.

Package structure

Throughout this document, I am using the following package structure:

  • @base-ui/x-date-pickers (and @base-ui/x-date-pickers-pro): The packages that contain the components built using the Base UI DX and potentially some higher level components. Should not have @mui/material as a dependency, and if possible not even @mui/system.
  • @mui/x-date-pickers (and @mui/x-date-pickers-pro): The package that contain the components built on top of Material UI (the equivalent of the current components).

@zucchini is a placeholder and will of course be replaced by the ne name of MUI X once decided.

New components

Picker

The Picker component would be the cornerstone for people that wants:

  1. To have a picker without bundling our styling strategy (@mui/system and @mui/material)
  2. To have a picker with a radically different UX on parts that can not be solved using only slots (e.g: someone who wants to use the TimeClock on desktop).

This component would replace most of the current useXXXPicker hook and the current PickersLayout component.
It would allow people to override only the part of the UI that they want to override, allowing them to migrate step by step from @mui/material without having to rebuild everything from day 1.

I am far from certain how this component should look like, the only thing I'm pretty sure about is that we need some tool like to give users an escape hatch when they need advanced customization (either for unstyled or to replace some views).

Mimimal example

A mimimal example using all our Material UI components would look something like this:

import { useDateManager } from '@base-ui/x-date-pickers/managers';
import { Picker } from '@base-ui/x-date-pickers/Picker';
import { DateField } from '@mui/x-date-pickers/DateField';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { DatePickerToolbar } from '@mui/x-date-pickers/DatePickerToolbar';
import { PickerResponsiveViewPopper } from '@mui/x-date-pickers/PickerResponsiveViewPopper';

function MyDatePicker(props) {
  const manager = useDateManager();

  return (
    <Picker.Root manager={manager} {...props}>
      <DateField />
      <PickerResponsiveViewPopper>
        <Picker.ViewLayout>
          <DatePickerToolbar />
          <DateCalendar />
        <Picker.ViewLayout>
      </PickerResponsiveViewPopper>
    </Picker.Root>
  )
}

Custom field

Using Picker, people could easily swap their built-in field for one that is built using PickerField (see #14496 (comment) for more details)

import { useDateManager } from '@base-ui/x-date-pickers/managers';
import { Picker } from '@base-ui/x-date-pickers/Picker';
import { PickerField } from '@base-ui/x-date-pickers/PickerField';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { DatePickerToolbar } from '@mui/x-date-pickers/DatePickerToolbar';
import { PickerResponsivePopup } from '@mui/x-date-pickers/PickerResponsivePopup';

function MyDatePicker(props) {
  const manager = useDateManager();

  return (
    <Picker.Root manager={manager} {...props}>
      <PickerField.Root manager={manager}>{ ... }</PickerField.Root>
      <PickerResponsivePopup>
        <Picker.ViewLayout>
          <DatePickerToolbar />
          <DateCalendar />
        <Picker.ViewLayout>
      </PickerResponsivePopup>
    </Picker.Root>
  )
}

Note that for now the rest of the UI is style using Material UI, people can migrate the field to their design system and then tackle the other parts of the UI they think are the most importants.

Modal / popover management

The example above are using PickerResponsivePopup for managing the opening of their views.
This component would be built on top of Picker.ResponsivePopup and apply some MUI style to it.
Picker.ResponsivePopup would then use either Popover or Dialog from Base UI depending on the current variant of the picker (currently "mobile", "desktop" but we can change this nomenclature if we want to).

Here is what it would look like for people using the responsive approach but without Material UI:

import { useDateManager } from '@base-ui/x-date-pickers/managers';
import { Picker } from '@base-ui/x-date-pickers/Picker';
import { PickerField } from '@base-ui/x-date-pickers/PickerField';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { DatePickerToolbar } from '@mui/x-date-pickers/DatePickerToolbar';

function MyDatePicker(props) {
  const manager = useDateManager();

  return (
    <Picker.Root manager={manager} {...props}>
      <PickerField.Root manager={manager}>{ ... }</PickerField.Root>
      <Picker.ResponsivePopup>
        <Picker.ViewLayout>
          <DatePickerToolbar />
          <DateCalendar />
        <Picker.ViewLayout>
      </Picker.ResponsivePopup>
    </Picker.Root>
  )
}

To customize when Popover and Modal should be used, we can keep an API similar to today with desktopModeMediaQuery (which is very poorly documented btw):

import { useDateManager } from '@base-ui/x-date-pickers/managers';
import { Picker } from '@base-ui/x-date-pickers/Picker';
import { PickerField } from '@base-ui/x-date-pickers/PickerField';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { DatePickerToolbar } from '@mui/x-date-pickers/DatePickerToolbar';

function MyDatePicker(props) {
  const manager = useDateManager();

  return (
    // popoverModeMediaQuery is applied on the root so both the trigger and the popup component can adapt.
    <Picker.Root manager={manager} {...props} popoverModeMediaQuery="@media (max-width: 1250px)">
      <PickerField.Root manager={manager}>{ ... }</PickerField.Root>
      <Picker.ResponsivePopup>
        <Picker.ViewLayout>
          <DatePickerToolbar />
          <DateCalendar />
        <Picker.ViewLayout>
      </Picker.ResponsivePopup>
    </Picker.Root>
  )
}

Or people can then decide to directly use the Base UI components to get rid of the responsive aspect if they want:

import { useDateManager } from '@base-ui/x-date-pickers/managers';
import { Picker } from '@base-ui/x-date-pickers/Picker';
import { PickerField } from '@base-ui/x-date-pickers/PickerField';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { DatePickerToolbar } from '@mui/x-date-pickers/DatePickerToolbar';

function MyDatePicker(props) {
  const manager = useDateManager();

  return (
    <Picker.Root manager={manager} {...props}>
      // In the custom field, they must use `Popover.Trigger` for the opening button
      <PickerField.Root manager={manager}>{ ... }</PickerField.Root>
      <Popover.Positioner sideOffset={8}>
       <Popover.Popup>
        <Picker.ViewLayout>
          <DatePickerToolbar />
          <DateCalendar />
        <Picker.ViewLayout>
        </Popover.Popup>
      </Popover.Positioner>
    </Picker.Root>
  )
}

By default, our popper management would be encapsulated in a PickerResponsiveViewPopper component exposed by @mui/x-date-picers. But this component uses @mui/material so it is important that people wanting to stop bundling our library are able to replace it with their own component.
It is also a good opportunity to allow people to control if they want a modal, a drop down etc...

To allow overriding this component, we could expose a component in Picker that takes a render prop as its children and passes all the necessary information to map to the popper:

import { useDateManager } from '@base-ui/x-date-pickers/managers';
import { Picker } from '@base-ui/x-date-pickers/Picker';
import { Modal } from '@mantine/core';

function MyDatePicker(props) {
  const manager = useDateManager();

  return (
    <Picker.Root manager={manager} {...props}>
      {...}
      <Picker.Popper>
        {(popperState) => (
          <Modal opened={popperState.open} onClose={popperState.onClose}>
            <Picker.ViewLayout>
              {...}
            <Picker.ViewLayout>
          </Modal>
        )}
      </Picker.Popper>
    </Picker.Root>
  )
}

Custom view component

To replace the viewManagers API which is quite hard to understand, we could introduce a Picker.View component that allows to conditionnally render the view depending on the current view:

import { useDateManager } from '@base-ui/x-date-pickers/managers';
import { Picker } from '@base-ui/x-date-pickers/Picker';
import { DateField } from '@mui/x-date-pickers/DateField';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { DatePickerToolbar } from '@mui/x-date-pickers/DatePickerToolbar';
import { PickersPopper } from '@mui/x-date-pickers/PickersPopper';

function MyDatePicker(props) {
  const manager = useDateManager();

  return (
    <Picker.Root manager={manager} {...props}>
      <DateField />
      <PickersPopper>
        <Picker.ViewLayout>
          <DatePickerToolbar />
          <Picker.View show="day">
            <MyDayView />
          <Picker.View />
          <Picker.View show="year">
            <MyYearView />
          <Picker.View />
        <Picker.ViewLayout>
      </PickersPopper>
    </Picker.Root>
  )
}

It would even easily support nested views out of the box.
Here is what it would look like if someone wanted to only override the year view:

<Picker.View show={['day', 'month', 'year']}
  <DateCalendar.Root>
    <DateCalendarHeader />
    <DateCalendar.Content>
      <Picker.View show="day"><DayCalendar /></Picker.View>
      <Picker.View show="month"><MonthCalendar /></Picker.View>
      <Picker.View show="year"><MyYearView /></Picker.View>
    </DateCalendar.Content>
  </DateCalendar.Root>
</Picker.View>

Potential roadmap

  • v8.0.0 alpha

    • Settle on a package structure for our Material UI components and our Material-less components (that can use Base UI DX or that can use DX with slots depending on future product decisions)
  • v8.X.X :

    • Release the PickerField component as unstable (see [pickers] Improve the DX of custom fields #14496 (comment) for more details about the field's roadmap)
    • Release the Picker component as unstable
    • Open a Github issue for every component that could benefit from a Base UI DX version
    • Start developping the Base UI DX version of components with high traction depending on our bandwidth
  • v9.0.0:

    • Make Picker stable
    • BC: Refactor the picker components to use Picker for their structure
    • Continue developping the Base UI DX version of components with high traction depending on our bandwidth
    • BC: Refactor the component that have a Base UI DX counterpart to use it (e.g: if we have a DateCalendar component with a Base UI DX, then our Material UI DateCalendar should be re-build using it as much as possible).
@github-actions github-actions bot added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Sep 24, 2024
@flaviendelangle flaviendelangle added component: pickers This is the name of the generic UI component, not the React module! customization: logic Logic customizability design: base-ui It's about the headless components. and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Sep 24, 2024
@flaviendelangle flaviendelangle self-assigned this Sep 24, 2024
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 design: base-ui It's about the headless components.
Projects
None yet
Development

No branches or pull requests

1 participant