wix-style-react
Version:
492 lines (411 loc) • 15.8 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import isSameDay from 'date-fns/isSameDay';
import setYear from 'date-fns/setYear';
import setMonth from 'date-fns/setMonth';
import setDate from 'date-fns/setDate';
import Popover from '../Popover';
import Calendar from '../Calendar';
import DateInput from './DateInput';
import { PopoverCommonProps } from '../common/PropTypes/PopoverCommon';
import deprecationLog from '../utils/deprecationLog';
import { st, classes } from './DatePicker.st.css';
import { dataHooks } from './constants';
import { WixStyleReactEnvironmentContext } from '../WixStyleReactEnvironmentProvider/context';
import { supportedWixlocales } from 'wix-design-systems-locale-utils';
/**
* DatePicker component
*
* ### Keyboard support
* * `Left`: Move to the previous day.
* * `Right`: Move to the next day.
* * `Up`: Move to the previous week.
* * `Down`: Move to the next week.
* * `PgUp`: Move to the previous month.
* * `PgDn`: Move to the next month.
* * `Home`: Move to the previous year.
* * `End`: Move to the next year.
* * `Enter`/`Esc`/`Tab`: close the calendar. (`Enter` & `Esc` calls `preventDefault`)
*
*/
export default class DatePicker extends React.PureComponent {
static displayName = 'DatePicker';
static defaultProps = {
filterDate: () => true,
rtl: false,
width: '150px',
zIndex: 1,
disabled: false,
inputDataHook: dataHooks.datePickerInput,
popoverProps: {
zIndex: 1,
},
disableKeyboardType: true,
onChange: () => {},
dateStyle: 'short',
};
constructor(props) {
super(props);
const initialOpen = props.initialOpen && !props.disabled;
this.state = {
value: props.value || new Date(),
isOpen: initialOpen,
isDateInputFocusable: !props.initialOpen,
inputValue: props.value || '',
};
if (this.props.dateFormat || this.props.dateFormatV2) {
deprecationLog(
'dateFormat and dateFormatV2 props are deprecated and will be removed in the next major release. Date format is determined for you automatically.',
);
}
if (this.props.disableKeyboardType) {
deprecationLog(
'disableKeyboardType is set to true by default, but in next major version this will be false. Make sure to handle keyboarboard input with onChange or sets disableKeyboardType prop to false.',
);
}
}
openCalendar = () => {
if (!this.state.isOpen && !this.props.readOnly) {
this.setState({
isOpen: true,
isDateInputFocusable: false,
value: this.props.value || new Date(),
});
}
};
closeCalendar = () => {
this.setState({ isOpen: false }, () => {
if (this.props.onClose) {
this.props.onClose();
}
});
/*
to fix case when user press tab in opened Calendar and:
1. Calendar become closed
2. Focus triggered
3. openCalendar triggered by focus
4. Calendar become visible
5. Looks like nothing happen
We need to do such steps:
1. Close calendar(with isDateInputFocusable: false)
2. After calendar is closed, on next event loop(after focus is fired) make isDateInputFocusable: focusable
to allow user to press tab in future and open Calendar
*/
setTimeout(() => this.makeInputFocusable());
};
makeInputFocusable = () => this.setState({ isDateInputFocusable: true });
_handleInputChange = ({ dateVal, textVal }) => {
this._saveNewValue(dateVal);
this.setState({ inputValue: textVal });
};
componentDidUpdate = prevProps => {
const { value: prevValue } = prevProps;
const { value: newValue } = this.props;
if (newValue !== prevValue) {
if (newValue) {
this._saveNewValue(newValue);
return this.setState({ inputValue: newValue });
}
const value = this._transformDate(new Date(), prevValue);
return this.setState({ inputValue: '', value });
}
};
_transformDate = (value, oldValue) =>
[
[value.getFullYear(), setYear],
[value.getMonth(), setMonth],
[value.getDate(), setDate],
].reduce(
(_value, [datePart, setter]) => setter(_value, datePart),
oldValue,
);
_saveNewValue = (value, modifiers = {}) => {
if (modifiers.disabled) {
return;
}
const isChanged = !isSameDay(value, this.props.value);
if (isChanged) {
const oldValue =
this.props.value || new Date(new Date().setHours(0, 0, 0, 0));
const newValue = this._transformDate(value, oldValue);
this.setState({ value: newValue }, () => this.props.onChange(newValue));
}
};
_handleKeyDown = event => {
// TODO: dirty for now
if (!this.state.isOpen) {
this.openCalendar();
}
/* keyHandler(this.state.value);*/
};
_renderInput = () => {
const {
inputDataHook,
disabled,
placeholderText,
readOnly,
status,
statusMessage,
customInput,
dateFormatV2,
dateFormat,
inputProps = {},
size,
clearButton,
onClear,
disableKeyboardType,
dateStyle,
} = this.props;
const { onFocus, ...inputPropsRest } = inputProps;
return (
<DateInput
dataHook={inputDataHook}
className={classes.input}
value={this.state.inputValue}
/* This line normally does nothing, because once clicked, component is already focused, hence onFocus
kicks in and open the calendar.
Why do we still keep this line? Backward compatibility for clients that test the component and simulate click
without simulating focus first. */
onInputClicked={this.openCalendar}
disabled={disabled}
readOnly={readOnly}
placeholder={placeholderText}
onFocus={e => {
onFocus && onFocus(e);
this.openCalendar(e);
}}
onKeyDown={this._handleKeyDown}
tabIndex={this.state.isDateInputFocusable ? 0 : -1}
status={status}
statusMessage={statusMessage}
autoSelect={false}
dateFormat={dateFormat}
dateFormatV2={dateFormatV2}
customInput={customInput}
locale={this._getLocale()}
dateStyle={dateStyle}
size={size}
clearButton={clearButton}
onClear={onClear}
onChange={this._handleInputChange}
disableEditing={disableKeyboardType}
{...(customInput ? customInput.props : {})}
{...inputPropsRest}
/>
);
};
_getLocale() {
if (typeof this.props.locale !== 'string') {
deprecationLog(
'<DatePicker/> prop "locale" with value `date-fns locale object` is deprecated and will be removed in next major release, please pass a string instead',
);
}
return this.props.locale || this.context.locale || 'en';
}
render() {
const {
className,
showMonthDropdown,
showYearDropdown,
filterDate,
excludePastDates,
rtl,
shouldCloseOnSelect,
width,
calendarDataHook,
twoMonths,
zIndex,
dataHook,
popoverProps,
firstDayOfWeek,
leftArrowAriaLabel,
leftArrowAriaLabelledBy,
rightArrowAriaLabel,
rightArrowAriaLabelledBy,
monthDropdownAriaLabel,
monthDropdownAriaLabelledBy,
yearDropdownAriaLabel,
yearDropdownAriaLabelledBy,
disableKeyboardType,
} = this.props;
const { isOpen, value } = this.state;
const popoverUpdatedProps = {
placement: rtl ? 'top-end' : 'top-start',
...popoverProps,
};
const calendarProps = {
dataHook: dataHooks.datePickerCalendar,
className: classes.calendar,
locale: this._getLocale(),
showMonthDropdown,
showYearDropdown,
filterDate,
excludePastDates,
rtl,
onChange: this._saveNewValue,
onClose: this.closeCalendar,
value,
shouldCloseOnSelect,
numOfMonths: twoMonths ? 2 : 1,
firstDayOfWeek,
leftArrowAriaLabel,
leftArrowAriaLabelledBy,
rightArrowAriaLabel,
rightArrowAriaLabelledBy,
monthDropdownAriaLabel,
monthDropdownAriaLabelledBy,
yearDropdownAriaLabel,
yearDropdownAriaLabelledBy,
};
return (
<div
className={st(classes.root, { disableKeyboardType }, className)}
data-hook={dataHook}
style={{ width: width }}
>
<Popover
className={classes.popover}
dataHook={dataHooks.datePickerPopover}
onClickOutside={this.closeCalendar}
appendTo="parent"
shown={isOpen}
zIndex={zIndex}
{...popoverUpdatedProps}
>
<Popover.Element>
<div
className={classes.inputContainer}
data-hook={dataHooks.datePickerInputContainer}
>
{this._renderInput()}
</div>
</Popover.Element>
<Popover.Content>
<div data-hook={calendarDataHook}>
<Calendar {...calendarProps} />
</div>
</Popover.Content>
</Popover>
</div>
);
}
}
DatePicker.contextType = WixStyleReactEnvironmentContext;
DatePicker.propTypes = {
/** Specify a single CSS class name to be appended to the root element */
className: PropTypes.string,
/** Applies as data-hook HTML attribute that can be used in the tests */
dataHook: PropTypes.string,
/** Focus selected day automatically when component mounts or updates */
autoFocus: PropTypes.bool,
/** Override a field with a custom input element. If you only need to pass custom props to the `<Input/>`, then use `inputProps` instead. */
customInput: PropTypes.node,
/** Allows you to pass default Input component properties */
inputProps: PropTypes.object,
/** this prop is deprecated and should not be used
* @deprecated
*/
dateFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/** this prop is deprecated and should not be used
* @deprecated
*/
dateFormatV2: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/**
##### Add an indication under a specific date.
Function returns the indication node of a specific date or null if this day doesn't have an indication.
* - `param` {date: Date, isSelected: boolean }
* - `date` - a date
* - `isSelected` - whether this date is the selected date
* - `return` {React.node} - the indication node of a specific date or null if this day doesn't have an indication.
*/
dateIndication: PropTypes.func,
/** Specify date picker instance locale */
locale: PropTypes.oneOfType([
PropTypes.oneOf(supportedWixlocales),
PropTypes.shape({
code: PropTypes.string,
formatDistance: PropTypes.func,
formatRelative: PropTypes.func,
localize: PropTypes.object,
formatLong: PropTypes.object,
match: PropTypes.object,
options: PropTypes.object,
}),
]),
/** Sets date format of locale */
dateStyle: PropTypes.oneOf(['short', 'medium']),
/** Specify whether a field should be disabled or not */
disabled: PropTypes.bool,
/** Specify whether past dates should be selectable or not */
excludePastDates: PropTypes.bool,
/**
* ##### Specify selectable dates:
* * `param` {Date} `date` - a date to check
* * `return` {boolean} - true if `date` should be selectable, false otherwise
*/
filterDate: PropTypes.func,
/** Applies a data-hook HTML attribute for date picker input */
inputDataHook: PropTypes.string,
/** Applies a data-hook HTML attribute for date picker calendar view */
calendarDataHook: PropTypes.string,
/** Defines a placeholder of the field */
placeholderText: PropTypes.string,
/** Specify whether RTL mode is enabled or not. When true, the keyboard navigation will be changed, meaning pressing on the right arrow will navigate to the previous day, and pressing on the left arrow will navigate to the next day. */
rtl: PropTypes.bool,
/** Defines the selected date */
value: PropTypes.object,
/** Specify whether the calendar will be initially visible or not */
initialOpen: PropTypes.bool,
/** Controls the status of a field */
status: PropTypes.oneOf(['error', 'warning', 'loading']),
/** Defines the status message to be displayed on status icon hover. If not given or empty, the tooltip won’t be shown. */
statusMessage: PropTypes.node,
/** Sets the width of picker input in pixels or percentage */
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Set a desired z-index for date picker popover */
zIndex: PropTypes.number,
/** Allows you to pass popover properties. The default placement value depends on the rtl prop - would be 'top-start' when rtl=false and 'top-end' in case of rtl=ture. */
popoverProps: PropTypes.shape(PopoverCommonProps),
/** Specify the starting day of a week, allowing only from 0 to 6 (Sunday to Saturday). The default value is 1 which means Monday. */
firstDayOfWeek: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]),
/** Specifies the size of the input */
size: PropTypes.oneOf(['small', 'medium', 'large']),
/** Specify whether date picker input is readOnly or not */
readOnly: PropTypes.bool,
/** Sets today's date. The today indication is added automatically according to the user timezone but in some cases, we need the ability to add the today indication based on the business timezone. */
today: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
/** Display a clear button (x) on a non-empty field */
clearButton: PropTypes.bool,
/** Provides a callback function when day in selected in the calendar */
onChange: PropTypes.func.isRequired,
/** Displays clear button (X) on a non-empty input and calls callback with no arguments */
onClear: PropTypes.func,
/** Defines a callback function that is called whenever a user presses escape, clicks outside of the element or a date is selected and `shouldCloseOnSelect` is set. Receives an event as a first argument. */
onClose: PropTypes.func,
/** Defines a callback function that is called with the date of the first day of the month whenever the user selects a month in the calendar */
onMonthChange: PropTypes.func,
/** Disable typing the in the input. When true, choosing a date is possible only by picking from the calendar. Default: true. */
disableKeyboardType: PropTypes.bool,
/** Displays a selectable yearDropdown */
showYearDropdown: PropTypes.bool,
/** Displays a selectable monthDropdown */
showMonthDropdown: PropTypes.bool,
/** Specify whether the calendar closes on day selection */
shouldCloseOnSelect: PropTypes.bool,
/** Defines a string value that labels the months dropdown in calendar header */
monthDropdownAriaLabel: PropTypes.string,
/** Identifies the element that labels the months dropdown in calendar header */
monthDropdownAriaLabelledBy: PropTypes.string,
/** Defines a string value that labels the years dropdown in calendar header */
yearDropdownAriaLabel: PropTypes.string,
/** Identifies the element that labels the years dropdown in calendar header */
yearDropdownAriaLabelledBy: PropTypes.string,
/** Defines a string value that labels the left arrow in calendar header */
leftArrowAriaLabel: PropTypes.string,
/** Identifies the element that labels the left arrow in calendar header */
leftArrowAriaLabelledBy: PropTypes.string,
/** Defines a string value that labels the right arrow in calendar header */
rightArrowAriaLabel: PropTypes.string,
/** Identifies the element that labels the right arrow in calendar header */
rightArrowAriaLabelledBy: PropTypes.string,
};