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

Display the day picker in an overlay #98

Closed
gpbl opened this issue Nov 21, 2015 · 33 comments
Closed

Display the day picker in an overlay #98

gpbl opened this issue Nov 21, 2015 · 33 comments
Labels
help wanted Extra attention is needed

Comments

@gpbl
Copy link
Owner

gpbl commented Nov 21, 2015

A common use for a date picker displaying it when focusing on an input field, so it would be nice to have an example in the examples app showing how to implement it. The example may use react-overlays.

@gpbl
Copy link
Owner Author

gpbl commented Nov 21, 2015

I recall @kblcuk talking about it in the gitter room, maybe he has already an example ready to use 😀

@kblcuk
Copy link
Contributor

kblcuk commented Nov 22, 2015

Pretty close to that -- in my case it's a button that displays date set; something like this

var datePicker = (<Popover id={'date-range-popover'}>
  <a onClick={this.handleResetTouchTap}>Reset</a>
  <DayPicker
      onDayTouchTap={this.handleDayClick}
      onDayClick={this.handleDayClick}
      enableOutsideDays
      />
</Popover>);

return (<div>
  <label htmlFor="date-selector">Timed to</label>
  <OverlayTrigger trigger="click" rootClose placement="bottom" overlay={datePicker}>
    <Button name="date-selector"
            id="date-selector"
            bsSize="xs"
        >{this.state.dateLabel}
    </Button>
  </OverlayTrigger>
</div>);

I'll try to add an example at some point if I'll figure out how examples are done :)

@gpbl
Copy link
Owner Author

gpbl commented Nov 22, 2015

Thanks @kblcuk 🤗

If you want to create a new example:

  1. duplicate the simplest one
  2. change the name of the exported class/function
  3. add the new component to the EXAMPLES array

it should appear in the example site:

cd examples
npm install
npm start
open http://localhost:3000

(not sure if it will work with any node environment: I use node 5 on OSX)

@gpbl
Copy link
Owner Author

gpbl commented Nov 22, 2015

@kblcuk I think the difficult part with this implementation is having the day picker disappear when clicking outside it – while it should stay visible when clicking inside it (e.g. to navigate between months).

I remember I had to fight enough for making it working this way, using timeouts and attaching events to the body. It wasn't that easy. My understanding is that react-overlays may help.

@kblcuk
Copy link
Contributor

kblcuk commented Nov 22, 2015

Hm, I had similar issue related to react-bootstrap dropdown & input, but not with the date picker: https://gitter.im/react-bootstrap/react-bootstrap?at=5609585ba5b78d0e12a3e009

@gpbl
Copy link
Owner Author

gpbl commented Nov 23, 2015

(Aside: also #100)

@bsr203
Copy link

bsr203 commented Jan 19, 2016

+1 I too would like to see an example of an overlay triggered through input + button combo. It is amazing how flexible and well written this package is. thank you.

considering to switch from http://jquense.github.io/react-widgets/docs/#/datetime-picker and would be great to see how easy it to have a drop down.

@gpbl
Copy link
Owner Author

gpbl commented Jan 19, 2016

Thanks @bsr203! I hope it will be easy 😁 I plan to work on it on the next days...

@bsr203
Copy link

bsr203 commented Jan 20, 2016

hi. modified your selectableDay example, and it worked like a charm. It need better styling and stuff, but the integration was trivial. we may need to extend it with an input box and an iconed button. but, happy that this library is easily adaptable. thanks again for your hard work. cheers.

import React from "react";
import {findDOMNode} from "react-dom";
import DayPicker, { DateUtils } from "react-day-picker";
import {Overlay} from "react-overlays";

import "react-day-picker/lib/style.css";

export default class OverlayPicker extends React.Component {

  state = {
    show: false,
    selectedDay: null
  }

  handleDayClick(e, day, modifiers) {
    this.setState({
      selectedDay: modifiers.indexOf("selected") > -1 ? null : day
    });
  }

  toggle() {
    return this.setState({ show: !this.state.show });
  }

  render() {
    const { selectedDay } = this.state;

    return (
      <div style={{position : "relative"}}>
        <p>
          Selected: { selectedDay && selectedDay.toLocaleDateString() }
        </p>
        <div>
          <button ref="target" onClick={this.toggle.bind(this)}>
            picker
          </button>
          <Overlay
            show={this.state.show}
            onHide={() => this.setState({ show: false })}
            placement={this.state.placement}
            container={this}
            rootClose
            target={ () => findDOMNode(this.refs.target)}
          >
            <DayPicker
              style={{position : "absolute"}}
              modifiers={{
                selected: day => DateUtils.isSameDay(selectedDay, day)
              }}
              onDayClick={ this.handleDayClick.bind(this) }
            />
          </Overlay>
        </div>
      </div>
    );
  }
}

@gpbl
Copy link
Owner Author

gpbl commented Jan 20, 2016

Thanks @bsr203, looking great, and easy as I hoped 😍 Can't wait to try it and add your example to the others. Just have some patience, as I'm a bit busy these days...

@bsr203
Copy link

bsr203 commented Jan 21, 2016

sure no problem. I just wanted to validate it, and glad to know that it works quite well.

One thing missing for me is to support a time picker too (as in here ) Do you strip off the time component, or is it possible to extend this library to have a time picker. thank you.

@gpbl
Copy link
Owner Author

gpbl commented Jan 22, 2016

I was playing with react-overlays but it doesn't help to display (or hide) the overlay when focusing (or blurring) a text input 😞 Which is actually the goal of this issue.

There's an OverlayTrigger component but it is included in react-bootstrap: I'd prefer to avoid this big dependency. I've found pui-react-overlay-trigger which may help, need to try.

@bsr203 I don't plan to add a time picker, it goes out from the scope of this component.

@gpbl gpbl added the help wanted Extra attention is needed label Jan 22, 2016
@srph
Copy link
Contributor

srph commented Jan 23, 2016

I'm not actually using an overlay, just simple CSS (absolute daypicker; relative parent). Does that count? The only problem we had was unable to hide it when we focus on the DayPicker (which is why I sent #122); #123 fixes it.

I can submit an example if that counts.

@philippe-git
Copy link

Hey, just wanted to drop a note saying that the day picker works wonderfully well with react-simple-dropdown when the use-case is to click on a button to show the day picker 😄 It was really quick and smooth to implement, simply dropping DayPicker inside DropdownContent (the component meant to contain the dropdown's contents); only had to make sure that clicks on DropdownContent didn't bubble since sometimes click targets are removed from the DOM (e.g. http://cl.ly/0u0q0Y250x2d), and react-simple-dropdown checks where the event target is in the DOM to decide whether to hide the dropdown's contents or not :)

@huonghk
Copy link

huonghk commented Jul 20, 2016

I have a solution which no need to use another library. Just show date picker when we focus on input, and detect when we click in outside of input and date picker to hide it. It's work for me. Hope it helpful for someone :)

Here is source code:

import React from 'react';
import moment from 'moment';

import DayPicker, { DateUtils } from 'react-day-picker';

import 'react-day-picker/lib/style.css';

export default class InputField extends React.Component {

  constructor(props) {
    super(props);
    this.handleDayClick = this.handleDayClick.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
    this.showCurrentDate = this.showCurrentDate.bind(this);

    this.handleFocus = this.handleFocus.bind(this); // handle focus on input
    this.handleClick = this.handleClick.bind(this); // handle click event in document

    this.state = {
      value: moment().format('L'), // The value of the input field
      month: new Date(), // The month to display in the calendar
      show: false // The show to show/hide calander
    };
  }

  showCurrentDate() {
    this.refs.daypicker.showMonth(this.state.month);
  }

  handleInputChange(e) {
    const { value } = e.target;

    // Change the current month only if the value entered by the user
    // is a valid date, according to the `L` format
    if (moment(value, 'L', true).isValid()) {
      this.setState({
        month: moment(value, 'L').toDate(),
        value,
      }, this.showCurrentDate);
    } else {
      this.setState({ value }, this.showCurrentDate);
    }
  }

  handleDayClick(e, day) {
    this.setState({
      value: moment(day).format('L'),
      month: day,
    });
  }

  handleFocus() {
    this.showCurrentDate();
    this.setState(
      show: true
    );
  }

  componentWillMount() {
    document.addEventListener('click', this.handleClick, false);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.handleClick, false);
  }

  handleClick(event) {
    // detect where click event has occured
    // if click on input or calendar (date picker wrapper) -> no need to hide date picker
    // if click on out side element -> set state show = false -> to hide date piker
    if (this.refs.input.contains(event.target) || this.refs.calendar.contains(event.target)) {
      console.log('click inside');
    } else {
      console.log('click outside');
      this.setState({
        show: false
      });
    }
  }

  render() {
    const selectedDay = moment(this.state.value, 'L', true).toDate();

    return (
      <div>
        <p>
          <input
            ref="input"
            type="text"
            value={this.state.value}
            placeholder="YYYY-MM-DD"
            onChange={this.handleInputChange}
            onFocus={this.handleFocus}
          />
        </p>
        <div ref="calendar" style={this.state.show ? {} : { display: 'none' }}>
          <DayPicker
            ref="daypicker"
            initialMonth={this.state.month}
            selectedDays={day => DateUtils.isSameDay(selectedDay, day)}
            onDayClick={this.handleDayClick}
          />
        </div>
      </div>
    );
  }

}

@gpbl
Copy link
Owner Author

gpbl commented Jul 20, 2016

Thanks @huonghk I'll make an example with your code so we can have a starting point! However how does it work with the blur event? It seems to me the layer would stay open until the calendar is clicked - right?

@huonghk
Copy link

huonghk commented Aug 8, 2016

I think blur event can't resolve this problem. Imagine you set blur event for the Input to hide the Calendar. So it will work like this: when you try to move your mouse from the Input to the Calendar in order to select date, it will trigger blur event and hide Calendar immediately, and you have no way to select date. That's why in my solution above, I have to detect click event occurred in somewhere as long as is outside of the Input and the Calendar.

@gpbl
Copy link
Owner Author

gpbl commented Aug 10, 2016

@huonghk yes this is the reason why this issue is still open :) I am looking for a way to hide the date picker on blur.

I believe your solution doesn't work when navigating the form via keyboard, right? When the input field is focused and the user presses the tab key, the calendar won't hide.

A solution that could working "meh" is to set a timeout when the calendar is clicked or focused, but I haven't had yet the time to try it..

@srph
Copy link
Contributor

srph commented Aug 12, 2016

I've been pretty busy lately, (worse, I haven't re-read the new updates in this thread. Pardon me 😞 ).

IIRC, I simply checked if the focused element is either the date picker or the input. I also used react-gateway to mount (aka sub render) the whole thing. It worked pretty great.

Gist:

  • On click
    • If the picker is closed and the input was clicked (/focused): open picker
    • If the picker is open and anything other than the input and calendar was clicked: close picker
import React, {Component} from 'react';
import {findDOMNode} from 'react-dom';
import {Gateway} from 'react-gateway';
import DayPicker from 'react-day-picker';

export default class CalendarInput extends Component {
  state = {
    open: false
  };

  componentDidMount() {
    document.addEventListener('click', this.handleClick);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.handleClick);
  }

  render() {
    return (
      <div>
        <input type="text" ref="input">

        <Gateway>
          {this.state.open ? <DayPicker ref="calendar" /> : null}
        </Gateway>
      </div>
    );
  }

  handleClick = (evt) => {
    const {open} = this.state;
    const {calendar, input} = this.refs;
    const active = document.activeElement;

    if ( !open && active === input ) {
      this.setState({ open: true });
    }

    else if ( open && (active === input || active === findDOMNode(calendar)) ) {
      this.setState({ open: false });
    }
  }
}

I haven't verified if it's working. I just came up with this based on the pseudocode above.

One issue I can think of is the click handler executes before any value is assigned to activeElement (the focused element). But it shouldn't be hard to fix if ever.

Edit: My bad. I just re-read the whole thing, and the problem seems to be with the keyboard navigation. I've never attempted to solve this in the past since we used the day picker in a desktop touch screen.

@madisvain
Copy link

Any updates on this?

@frandsaw
Copy link
Contributor

#211

@gpbl gpbl closed this as completed in 4ee8a70 Sep 18, 2016
@gpbl gpbl mentioned this issue Sep 18, 2016
4 tasks
@gpbl
Copy link
Owner Author

gpbl commented Sep 18, 2016

@frandsaw work has been merged and published at http://www.gpbl.org/react-day-picker/examples/?overlay. The solution is a bit more complex from @srph's one, but IMHO it works better with keyboard's focus and blur events.

I think the example should be provided to be reused easily in new component, to be included in this library (see #213).

Thanks everybody for your invaluable contributions to this issue ❤️

@catamphetamine
Copy link

catamphetamine commented Mar 6, 2017

I'm adding my 2 cents regarding this issue:
https://github.com/halt-hammerzeit/react-responsive-ui/blob/master/source/date%20picker.js

Based on the code published in the comment above.

See it in action on this demo page (scroll down):
https://halt-hammerzeit.github.io/react-responsive-ui/

@gpbl
Copy link
Owner Author

gpbl commented May 10, 2017

For anyone interested, v5.5 includes a new DayPickerInput component that renders an input field interacting with the day picker in an overlay http://react-day-picker.js.org/examples/?input 😊

@catamphetamine
Copy link

@gpbl Looks good.
Requires moment bundled (which is quite big) but still it's a good component.
Btw, when I enter "fasdfasdfas" in the input and then focus out it doesn't restore the previously selected date.
I guess that's because the example uses the LL format.

@catamphetamine
Copy link

As for me, I ended up implementing the date parsing and formatting functions myself so that I don't bundle moment.js which is > 100 KiloBytes.

function format_date_custom(date, format)
{
	if (!(date instanceof Date))
	{
		return
	}

	const day   = date.getDate()
	const month = date.getMonth() + 1
	const year  = date.getFullYear()

	let text = format
		.replace('DD', pad_with_zeroes(String(day),   2))
		.replace('MM', pad_with_zeroes(String(month), 2))

	if (text.indexOf('YYYY') >= 0)
	{
		return text.replace('YYYY', pad_with_zeroes(String(year), 4))
	}

	if (text.indexOf('YY') >= 0)
	{
		return text.replace('YY', pad_with_zeroes(String(year % 100), 2))
	}
}


function parse_date_custom(string, format)
{
	if (!string)
	{
		return
	}

	let year = extract(string, format, 'YYYY')

	if (year === undefined)
	{
		year = extract(string, format, 'YY')

		if (year !== undefined)
		{
			const current_year = new Date().getFullYear()
			const current_year_century = current_year - current_year % 100
			year += current_year_century
		}
	}

	const month = extract(string, format, 'MM')
	const day   = extract(string, format, 'DD')

	if (year === undefined || month === undefined || day === undefined)
	{
		return console.error(`Couldn't parse date, perhaps an unsupported format: ${format}. Only DD, MM, YY and YYYY are supported.`)
	}

	const date = new Date
	(
		year,
		month - 1,
		day
	)

	// If `new Date()` returns "Invalid Date"
	// (sometimes it does)
	if (isNaN(date.getTime()))
	{
		return
	}

	return date
}

// + a couple of utility methods, see `react-responsive-ui`

Still a lot of code but nowhere near the size of moment.

@gpbl
Copy link
Owner Author

gpbl commented May 10, 2017

Thanks @halt-hammerzeit for the valid feedback!

I'm also not so happy about moment.js size. However, parsing a date from an input field that could be written in multiple language and/or in many formats is not easily done. We could use date-fns in the future instead. (here the idea of modularizing moment.js but it seems won't be done soon).

Btw, when I enter "fasdfasdfas" in the input and then focus out it doesn't restore the previously selected date.

Curious, why should it?

@catamphetamine
Copy link

@gpbl Well, most of the users would assume the text field allows editing the date, but since it's in "LL" format there's now way really to parse it so maybe it shouldn't parse it on focus out but still it shouldn't retain any changes then.
I mean, a "text field" concept is "editing" so it should be a button maybe, but a button would look weird.
So, my point is that "LL" is not the best format for a date input field.
If it was, say, "DD.MM.YYYY" then a pro user could easily enter dates manually and they would get parsed on focus out (const m = moment(value, format, true);).

@gpbl
Copy link
Owner Author

gpbl commented May 10, 2017

@halt-hammerzeit this applies to any format (that can be chosen with the format prop): the input is designed to allow the user type the date or using the day picker. I've used that format in the example because it's the requirement for the app I'm working on :)

@yairEO
Copy link

yairEO commented Sep 12, 2018

@gpbl - I personally think that you should de-couple the overlay code from the datepicker and have a separate dependency for another positioning lib like Popper which is a very strong one.

the reason is that it is highly probably developers already have a positioning script within their project and do not but the added bloat of having another positioning lib within react-day-picker itself (that handles the overlays) which i'm sure has a fair amount of code written there.

There should be a separate single-source positioning library for each front-end project and each 3rd party script should theoretically use the same 3rd-party position script.

it's highly wasteful for a project to install many packages, each having its own positioning script,
and I don't see a way to import react-day-picker without including it's positioning-system script along the way.

@gpbl
Copy link
Owner Author

gpbl commented Sep 12, 2018

@yairEO thanks for your feedback! I agree with you – but what should be the alternative? Write our own positioning system?

@yairEO
Copy link

yairEO commented Sep 12, 2018

The alternative would to write in the installation instructions that if a developer wishes to show the datepicker as an overlay, then they should also install [X,Y,Z... you chosose] and then import your scripts and the position script as well, and you can provide an example of how to wrap your date picker in an overlay such as popper or react-overlays as someone had mention in the comments.

Personally i think popper is the best choice because it's the most powerful positioning system I know of and the code is in very high standards.

I am probably actually going to do this now:
A button will open an overlay which will render a date-picker component and also a pre-defined date ranges, such as "last 24h", "last month", "last 6 months" ans so on, and the user could click on those or choose their own custom date range via your script. All this will be within the overlay Popper will show.

I personally really do not want an input element to trigger an overlay, like the one you show in the demo page. why must an input field trigger the overlay? why not a button? anything else? I don't even need or want an input field in my html...

@gpbl
Copy link
Owner Author

gpbl commented Sep 13, 2018

@yairEO you are right in many points. Thinking to move the input component to another package. #794

I don’t think it promotes a good UI pattern for the web. I know browser vendors failed to implement a picker so connected to an input field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

10 participants