DatePickerDialog – how to open Material-UI's DatePicker with a button (or any other element).

Material-UI has a very handy DatePicker component, but it's tied to a text input field for unknown reasons. You have to click that input field to open the DatePicker, and the input field will display the selected date. It's my UI though, and I wanted to open the DatePicker with a regular button click, or by focusing the button and pressing enter (i.e. when you're in a form and press tab to select the button).

It turns out Material-UI has a DatePickerDialog component that's used internally by the DatePicker, but they don't document the Dialog anywhere. You can see the dialog source here: DatePickerDialog. To make matters worse, the DatePickerDialog has several bugs:

  1. The open prop literally does nothing. It's specified as a prop type for the component, but never used. You need to get a reference to the dialog after it's been mounted into the DOM, and then call ref.show() or ref.dismiss() to control it.
  2. Its calendar rendering will break if you don't pass a firstDayOfWeek prop.
  3. It has very strange interaction with key capture events. If you use a key capture event to open it (e.g. onKeyUp) it will immediately close.
  4. It has very strange interaction with your mouse cursor. If the dialog opens under or near your cursor, and you immediately move your mouse as it's opening, there's a chance that the dialog will immediately close itself.

Here's what happens when you try to open the DatePickerDialog with a keyboard event – in this case, selecting the button and pressing 'Enter':

Dialog immediately closing after opening it with the 'Enter' key.

The same thing happens if the dialog opens under or near your cursor and you immediately move your mouse as it's opening.

When you try to open a DatePickerDialog and it closes itself.

Luckily solving #1 was pretty straightforward after a quick look through source code, and #2 was solved after searching StackOverflow. #3 and #4 were different stories though. After hours of googling, looking through source code, and searching GitHub issues, I've finally found the solution: use a reference to the dialog and overwrite its handleRequestClose and handleWindowKeyUp functions.

The DatePickerDialog passes its handleRequestClose function to the base Dialog component, but that base dialog will randomly call the close function immediately after it opens depending on where or how quickly you moved your mouse. Pretty weird stuff, and I still don't know why this happens, but overwriting that function "fixes" the DatePickerDialog component closing itself after mouse movement.

The handleWindowKeyUp function is used by the DatePickerDialog to determine if the user has pressed "Enter" on a calendar day and will then close it. There's a bug in there somewhere (again, I haven't figured out where) that will immediately close the dialog after it's been opened with a key event.

Overwriting the handleRequestClose function won't let the user close the dialog by clicking outside of it, but that's a small price to pay for a working date picker. Users can still close the dialog by selecting a date or pressing the "Cancel" button. Similarly, overwriting the handleWindowKeyUp function means users can't use their keyboard to select a calendar date, but the dialog can then be opened with a key event (e.g. pressing 'Enter' on a button).

A working example

This is a working example of Material-UI's DatePickerDialog opening with a button click – and focusing the button and pressing enter – rather than the text field used by the default DatePicker component.

import * as Dom from 'react-dom';
import * as React from 'react';
import DatePickerDialog from 'material-ui/DatePicker/DatePickerDialog';
import { MuiThemeProvider } from 'material-ui/styles';

// Material-UI needs the react-tap-event-plugin activated
require("react-tap-event-plugin")();

export class MyComponent extends React.Component {
    public state = {
        selectedDate: new Date()
    }

    private datePickerDialog;

    private setDatePickerDialogReference(ref) {
        // React passes undefined/null if the reference has been unmounted.
        if (ref) {
            // Overwrite the dialog's handleRequestClose and handleWindowKeyUp functions.
            ref.handleWindowKeyUp = (...args) => console.log("Dialog tried to call handleWindowKeyUp.");
            ref.handleRequestClose = (...args) => console.log("Dialog tried to call handleRequestClose.");
        }

        this.datePickerDialog = ref;
    }

    private openDatePicker() {
        this.datePickerDialog.show();
    }

    private openDatePickerOnEnter(e: React.KeyboardEvent<any>) {
        if (e.key === "Enter") {
            this.openDatePicker();
        }
    }

    private setDateReceived(date: Date) {
        this.setState({ selectedDate: date });
    }

    public render() {
        return (
            <div>
                <h1>{`Date Selected: ${this.state.selectedDate.toLocaleDateString()}`}</h1>
                <ol>
                    <li>{`Select the text field.`}</li>
                    <li>{`Press tab to select the button next.`}</li>
                    <li>{`Press enter with the button selected to open the DatePickerDialog.`}</li>
                </ol>
                <input
                    tabIndex={0}
                    type={"text"}
                    style={{ width: 300, margin: "10px, 0" }}
                    value={"Select me, then press tab and enter."}
                    readOnly />
                <div tabIndex={1}>
                    <button
                        type={"button"}
                        onKeyUp={e => this.openDatePickerOnEnter(e)}
                        onClick={e => this.openDatePicker()}>
                        {"Open DatePicker"}
                    </button>
                </div>
                <MuiThemeProvider>
                    <DatePickerDialog
                        ref={r => this.setDatePickerDialogReference(r)}
                        firstDayOfWeek={0 /* Must provide firstDayOfWeek or rendering of calendar will be broken. */}
                        autoOk={false /* Don't close the dialog until the user presses 'Okay'. */}
                        onAccept={date => this.setDateReceived(date)}
                        initialDate={this.state.selectedDate} />
                </MuiThemeProvider>
            </div>
        )
    }
}

(function () {
    Dom.render(<MyComponent />, document.getElementById("contenthost"));
}());

I've put this working example on GitHub, including instructions for running it.


Want to build Shopify apps for fun, glory and (most importantly) profit? I'm writing an update to The Shopify Development Handbook that will walk you through every step of the Shopify app development process using the new DotNet Core and Asp.Net Core. You can join the mailing list here to get an email as soon as it's launched. You'll also get a free guide which helps you get started building a Shopify application using the full .NET Framework and ASP.NET MVC.

Want to get started right now? The current version of The Shopify Development Handbook is on sale at Gumroad right now! Pick it up now and you'll get the updated version for free!


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.