mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
FieldDateInput
This commit is contained in:
parent
6229a9cecb
commit
1439ae72d8
13 changed files with 803 additions and 0 deletions
241
src/components/FieldDateInput/DateInput.css
Normal file
241
src/components/FieldDateInput/DateInput.css
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
:root {
|
||||
--selectionHeight: 36px;
|
||||
--hoveredOverlayColor: rgba(255, 255, 255, 0.2);
|
||||
--datepickerDistanceToTop: 252px;
|
||||
}
|
||||
|
||||
.inputRoot {
|
||||
/* Contain repainting to this component only */
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
||||
/* Override react-dates default styles to match input styles */
|
||||
|
||||
& :global(.SingleDatePicker) {
|
||||
display: block;
|
||||
}
|
||||
& :global(.SingleDatePicker_picker__directionLeft) {
|
||||
/* !important is added to top because react-dates uses inline style for height */
|
||||
/* Similar problem as in issue: https://github.com/airbnb/react-dates/issues/947 */
|
||||
top: 36px !important;
|
||||
width: 100%;
|
||||
min-height: calc(100vh - var(--datepickerDistanceToTop));
|
||||
background-color: var(--marketplaceColor);
|
||||
|
||||
@media (--viewportMedium) {
|
||||
/* !important is added to top because react-dates uses inline style for height */
|
||||
/* Similar problem as in issue: https://github.com/airbnb/react-dates/issues/947 */
|
||||
top: 40px !important;
|
||||
min-height: auto;
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
border-radius: 0 0 2px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
& :global(.DateInput_input) {
|
||||
@apply --marketplaceDefaultFontStyles;
|
||||
@apply --marketplaceInputStyles;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: var(--attentionColor);
|
||||
transition: border-bottom-color var(--transitionStyle);
|
||||
transition: all 0.15s ease-out;
|
||||
background: none;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
padding: 4px 0 9px 0;
|
||||
border-bottom-width: 3px;
|
||||
}
|
||||
}
|
||||
& :global(.SingleDatePickerInput) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
& :global(.DayPicker__horizontal) {
|
||||
margin: 0 auto;
|
||||
background-color: var(--marketplaceColor);
|
||||
box-shadow: none;
|
||||
}
|
||||
& :global(.CalendarMonth),
|
||||
& :global(.CalendarMonthGrid) {
|
||||
background-color: transparent;
|
||||
}
|
||||
& :global(.DateInput) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
& :global(.DayPicker_weekHeader) {
|
||||
color: var(--matterColorLight);
|
||||
top: 57px;
|
||||
}
|
||||
|
||||
& :global(.DayPicker_weekHeader_li) {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
& :global(.CalendarMonth_caption) {
|
||||
color: var(--matterColorLight);
|
||||
@apply --marketplaceH2FontStyles;
|
||||
margin: 1px 0 14px;
|
||||
font-weight: 400;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
& :global(.DayPickerNavigation_button) {
|
||||
color: var(--matterColorLight);
|
||||
border: 0;
|
||||
}
|
||||
& :global(.CalendarDay__default) {
|
||||
background-color: var(--marketplaceColor);
|
||||
border: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
& :global(.CalendarDay) {
|
||||
@apply --marketplaceH4FontStyles;
|
||||
color: var(--matterColorLight);
|
||||
border: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add an underline for '.renderedDay' */
|
||||
& :global(.CalendarDay__today .renderedDay) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--selectionHeight);
|
||||
background-image: url("data:image/svg+xml;utf8,<svg width='14' height='2' viewBox='0 0 14 2' xmlns='http://www.w3.org/2000/svg'><path d='M0 0h14v2H0z' fill='%23FFF' fill-rule='evenodd'/></svg>");
|
||||
background-position: center 28px;
|
||||
}
|
||||
|
||||
/* Remove default bg-color and use our extra span instead '.renderedDay' */
|
||||
& :global(.CalendarDay__hovered_span),
|
||||
& :global(.CalendarDay__selected_span) {
|
||||
background-image: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
& :global(.CalendarDay__hovered_span .renderedDay) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--selectionHeight);
|
||||
background-color: var(--hoveredOverlayColor);
|
||||
}
|
||||
& :global(.CalendarDay__selected_span .renderedDay) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--selectionHeight);
|
||||
background-color: var(--successColor);
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
/* Remove default bg-color and use our extra span instead '.renderedDay' */
|
||||
& :global(.CalendarDay__selected) {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
& :global(.CalendarDay__selected .renderedDay) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--selectionHeight);
|
||||
background-color: var(--successColor);
|
||||
border-radius: calc(var(--selectionHeight) / 2);
|
||||
}
|
||||
/* Remove default bg-color and use our extra span instead '.renderedDay' */
|
||||
& :global(.CalendarDay__after-hovered) {
|
||||
background-color: transparent;
|
||||
}
|
||||
& :global(.CalendarDay__after-hovered .renderedDay) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--selectionHeight);
|
||||
background-color: var(--hoveredOverlayColor);
|
||||
}
|
||||
& :global(.CalendarDay:hover .renderedDay) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--selectionHeight);
|
||||
background-color: var(--hoveredOverlayColor);
|
||||
}
|
||||
/* Remove default bg-color and use our extra span instead '.renderedDay' */
|
||||
& :global(.CalendarDay__blocked_out_of_range),
|
||||
& :global(.CalendarDay__blocked_out_of_range:active),
|
||||
& :global(.CalendarDay__blocked_out_of_range:hover) {
|
||||
background-color: transparent;
|
||||
color: var(--marketplaceColorDark);
|
||||
border: 0;
|
||||
}
|
||||
& :global(.CalendarDay__blocked_out_of_range .renderedDay) {
|
||||
background-color: transparent;
|
||||
}
|
||||
& :global(.DateInput_fang) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile margins mean that labels, inputs (incl separate borders) get margins,
|
||||
* but calendar popup doesn't.
|
||||
*/
|
||||
.withMobileMargins {
|
||||
& :global(.SingleDatePickerInput) {
|
||||
width: calc(100vw - 48px);
|
||||
margin: 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& :global(.SingleDatePicker_picker__directionLeft) {
|
||||
/* !important is added to top because react-dates uses inline style for height */
|
||||
/* Similar problem as in issue: https://github.com/airbnb/react-dates/issues/947 */
|
||||
top: 34px !important;
|
||||
border-top: 2px solid var(--matterColorDark);
|
||||
min-height: calc(100vh - var(--datepickerDistanceToTop) + 2px);
|
||||
|
||||
@media (--viewportMedium) {
|
||||
/* !important is added to top because react-dates uses inline style for height */
|
||||
/* Similar problem as in issue: https://github.com/airbnb/react-dates/issues/947 */
|
||||
top: 40px !important;
|
||||
border-top: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Create gutter between inputs */
|
||||
& :global(.DateInput) {
|
||||
width: 100%;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rootNextMonthIcon,
|
||||
.rootPreviousMonthIcon {
|
||||
stroke: var(--matterColorLight);
|
||||
fill: var(--matterColorLight);
|
||||
}
|
||||
224
src/components/FieldDateInput/DateInput.js
Normal file
224
src/components/FieldDateInput/DateInput.js
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* DateInput wraps SingleDatePicker from React-dates and gives a list of all default props we use.
|
||||
* Styles for SingleDatePicker can be found from 'public/reactDates.css'.
|
||||
*
|
||||
* N.B. *isOutsideRange* in defaultProps is defining what dates are available to booking.
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
import { bool, func, instanceOf, shape, string } from 'prop-types';
|
||||
|
||||
// react-dates needs to be initialized before using any react-dates component
|
||||
// Since this is currently only component using react-dates we can do it here
|
||||
// https://github.com/airbnb/react-dates#initialize
|
||||
import 'react-dates/initialize';
|
||||
import { SingleDatePicker, isInclusivelyAfterDay, isInclusivelyBeforeDay } from 'react-dates';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import config from '../../config';
|
||||
|
||||
import NextMonthIcon from './NextMonthIcon';
|
||||
import PreviousMonthIcon from './PreviousMonthIcon';
|
||||
import css from './DateInput.css';
|
||||
|
||||
export const HORIZONTAL_ORIENTATION = 'horizontal';
|
||||
export const ANCHOR_LEFT = 'left';
|
||||
|
||||
// Possible configuration options of React-dates
|
||||
const defaultProps = {
|
||||
initialDate: null, // Possible initial date passed for the component
|
||||
value: null, // Value should keep track of selected date.
|
||||
|
||||
// input related props
|
||||
id: 'dateInput',
|
||||
placeholder: null, // Handled inside component
|
||||
disabled: false,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
screenReaderInputMessage: null, // Handled inside component
|
||||
showClearDate: false,
|
||||
customCloseIcon: null,
|
||||
showDefaultInputIcon: false,
|
||||
customInputIcon: null,
|
||||
noBorder: true,
|
||||
block: false,
|
||||
small: false,
|
||||
|
||||
// calendar presentation and interaction related props
|
||||
renderMonth: null,
|
||||
orientation: HORIZONTAL_ORIENTATION,
|
||||
anchorDirection: ANCHOR_LEFT,
|
||||
horizontalMargin: 0,
|
||||
withPortal: false,
|
||||
withFullScreenPortal: false,
|
||||
initialVisibleMonth: null,
|
||||
firstDayOfWeek: config.i18n.firstDayOfWeek,
|
||||
numberOfMonths: 1,
|
||||
keepOpenOnDateSelect: false,
|
||||
reopenPickerOnClearDate: false,
|
||||
renderCalendarInfo: null,
|
||||
hideKeyboardShortcutsPanel: true,
|
||||
daySize: 38,
|
||||
isRTL: false,
|
||||
|
||||
// navigation related props
|
||||
navPrev: <PreviousMonthIcon />,
|
||||
navNext: <NextMonthIcon />,
|
||||
onPrevMonthClick() {},
|
||||
onNextMonthClick() {},
|
||||
onClose() {},
|
||||
transitionDuration: 200, // milliseconds between next month changes etc.
|
||||
|
||||
// day presentation and interaction related props
|
||||
renderCalendarDay: undefined, // If undefined, renders react-dates/lib/components/CalendarDay
|
||||
// day presentation and interaction related props
|
||||
renderDayContents: day => {
|
||||
return <span className="renderedDay">{day.format('D')}</span>;
|
||||
},
|
||||
enableOutsideDays: false,
|
||||
isDayBlocked: () => false,
|
||||
|
||||
// Stripe holds funds in a reserve for up to 90 days from charge creation.
|
||||
// outside range -><- today ... today+89 days -><- outside range
|
||||
isOutsideRange: day => {
|
||||
const daysCountAvailableToBook = 90;
|
||||
const endOfRange = daysCountAvailableToBook - 1;
|
||||
return (
|
||||
!isInclusivelyAfterDay(day, moment()) ||
|
||||
!isInclusivelyBeforeDay(day, moment().add(endOfRange, 'days'))
|
||||
);
|
||||
},
|
||||
isDayHighlighted: () => {},
|
||||
|
||||
// Internationalization props
|
||||
// Multilocale support can be achieved with displayFormat like moment.localeData.longDateFormat('L')
|
||||
// https://momentjs.com/
|
||||
displayFormat: 'ddd, MMM D',
|
||||
monthFormat: 'MMMM YYYY',
|
||||
weekDayFormat: 'dd',
|
||||
phrases: {
|
||||
closeDatePicker: null, // Handled inside component
|
||||
clearDate: null, // Handled inside component
|
||||
},
|
||||
};
|
||||
|
||||
class DateInputComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
focused: false,
|
||||
};
|
||||
|
||||
this.onDateChange = this.onDateChange.bind(this);
|
||||
this.onFocusChange = this.onFocusChange.bind(this);
|
||||
}
|
||||
|
||||
onDateChange(date) {
|
||||
const selectedDate = date instanceof moment ? date.toDate() : null;
|
||||
this.props.onChange({ date: selectedDate });
|
||||
}
|
||||
|
||||
onFocusChange(values) {
|
||||
const focused = values.focused;
|
||||
// SingleDatePicker requires 'onFocusChange' function and 'focused' boolean
|
||||
// but Fields of React-Form deals with onFocus & onBlur instead
|
||||
this.setState({ focused });
|
||||
|
||||
if (focused) {
|
||||
this.props.onFocus();
|
||||
} else {
|
||||
this.props.onBlur();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const {
|
||||
className,
|
||||
initialDate,
|
||||
intl,
|
||||
name,
|
||||
placeholderText,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
onDragStart,
|
||||
onDrop,
|
||||
phrases,
|
||||
screenReaderInputMessage,
|
||||
useMobileMargins,
|
||||
value,
|
||||
...datePickerProps
|
||||
} = this.props;
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const initialMoment = initialDate ? moment(initialDate) : null;
|
||||
|
||||
const date = value && value.date instanceof Date ? moment(value.date) : initialMoment;
|
||||
|
||||
const placeholder = placeholderText || intl.formatMessage({ id: 'FieldDateInput.placeholder' });
|
||||
|
||||
const screenReaderInputText =
|
||||
screenReaderInputMessage ||
|
||||
intl.formatMessage({ id: 'FieldDateInput.screenReaderInputMessage' });
|
||||
|
||||
const closeDatePickerText = phrases.closeDatePicker
|
||||
? phrases.closeDatePicker
|
||||
: intl.formatMessage({ id: 'FieldDateInput.closeDatePicker' });
|
||||
|
||||
const clearDateText = phrases.clearDate
|
||||
? phrases.clearDate
|
||||
: intl.formatMessage({ id: 'FieldDateInput.clearDate' });
|
||||
|
||||
const classes = classNames(css.inputRoot, className, {
|
||||
[css.withMobileMargins]: useMobileMargins,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<SingleDatePicker
|
||||
{...datePickerProps}
|
||||
focused={this.state.focused}
|
||||
onFocusChange={this.onFocusChange}
|
||||
date={date}
|
||||
onDateChange={this.onDateChange}
|
||||
placeholder={placeholder}
|
||||
screenReaderInputMessage={screenReaderInputText}
|
||||
phrases={{ closeDatePicker: closeDatePickerText, clearDate: clearDateText }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateInputComponent.defaultProps = {
|
||||
className: null,
|
||||
useMobileMargins: false,
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
DateInputComponent.propTypes = {
|
||||
className: string,
|
||||
focused: bool,
|
||||
initialDate: instanceOf(Date),
|
||||
intl: intlShape.isRequired,
|
||||
name: string.isRequired,
|
||||
|
||||
onChange: func.isRequired,
|
||||
onBlur: func.isRequired,
|
||||
onFocus: func.isRequired,
|
||||
onDragStart: func.isRequired,
|
||||
onDrop: func.isRequired,
|
||||
phrases: shape({
|
||||
closeDatePicker: string,
|
||||
clearDate: string,
|
||||
}),
|
||||
useMobileMargins: bool,
|
||||
placeholderText: string,
|
||||
screenReaderInputMessage: string,
|
||||
value: shape({
|
||||
date: instanceOf(Date),
|
||||
}),
|
||||
};
|
||||
|
||||
export default injectIntl(DateInputComponent);
|
||||
30
src/components/FieldDateInput/FieldDateInput.css
Normal file
30
src/components/FieldDateInput/FieldDateInput.css
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.fieldRoot {
|
||||
}
|
||||
|
||||
.pickerSuccess {
|
||||
& :global(.DateInput_input) {
|
||||
border-bottom-color: var(--successColor);
|
||||
}
|
||||
}
|
||||
|
||||
.pickerError {
|
||||
& :global(.DateInput_input) {
|
||||
border-bottom-color: var(--failColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile margins mean that labels, inputs (incl separate borders) get margins,
|
||||
* but calendar popup doesn't.
|
||||
*/
|
||||
.mobileMargins {
|
||||
width: calc(100% - 48px);
|
||||
margin: 0 24px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
53
src/components/FieldDateInput/FieldDateInput.example.js
Normal file
53
src/components/FieldDateInput/FieldDateInput.example.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable no-console */
|
||||
import React from 'react';
|
||||
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import moment from 'moment';
|
||||
import { Button } from '../../components';
|
||||
import { required, bookingDateRequired } from '../../util/validators';
|
||||
import FieldDateInput from './FieldDateInput';
|
||||
|
||||
const FormComponent = props => {
|
||||
const { form, handleSubmit, pristine, submitting, dateInputProps } = props;
|
||||
const submitDisabled = pristine || submitting;
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FieldDateInput {...dateInputProps} />
|
||||
<Button type="submit" disabled={submitDisabled} style={{ marginTop: '24px' }}>
|
||||
Select
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
FormComponent.propTypes = formPropTypes;
|
||||
|
||||
const defaultFormName = 'Styleguide.DateInput.Form';
|
||||
|
||||
const Form = reduxForm({
|
||||
form: defaultFormName,
|
||||
})(FormComponent);
|
||||
|
||||
export const Empty = {
|
||||
component: Form,
|
||||
props: {
|
||||
dateInputProps: {
|
||||
name: 'bookingDate',
|
||||
useMobileMargins: false,
|
||||
id: `${defaultFormName}.bookingDate`,
|
||||
label: 'Date',
|
||||
placeholderText: moment().format('ddd, MMMM D'),
|
||||
format: null,
|
||||
validate: [required('Required'), bookingDateRequired('Date is not valid')],
|
||||
onBlur: () => console.log('onBlur called from DateInput props.'),
|
||||
onFocus: () => console.log('onFocus called from DateInput props.'),
|
||||
},
|
||||
onChange: values => {
|
||||
const { date } = values;
|
||||
console.log('Changed to', moment(date).format('L'));
|
||||
},
|
||||
onSubmit: values => {
|
||||
console.log('Submitting a form with values:', values);
|
||||
},
|
||||
},
|
||||
group: 'custom inputs',
|
||||
};
|
||||
97
src/components/FieldDateInput/FieldDateInput.js
Normal file
97
src/components/FieldDateInput/FieldDateInput.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Provides a date picker for Redux Forms (using https://github.com/airbnb/react-dates)
|
||||
*
|
||||
* NOTE: If you are using this component inside BookingDatesForm,
|
||||
* you should convert value.date to start date and end date before submitting it to API
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
import { bool, object, string } from 'prop-types';
|
||||
import { Field } from 'redux-form';
|
||||
import classNames from 'classnames';
|
||||
import { ValidationError } from '../../components';
|
||||
|
||||
import DateInput from './DateInput';
|
||||
import css from './FieldDateInput.css';
|
||||
|
||||
class FieldDateInputComponent extends Component {
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
id,
|
||||
label,
|
||||
input,
|
||||
meta,
|
||||
useMobileMargins,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
if (label && !id) {
|
||||
throw new Error('id required when a label is given');
|
||||
}
|
||||
|
||||
const { touched, invalid, error } = meta;
|
||||
const value = input.value;
|
||||
|
||||
// If startDate is valid label changes color and bottom border changes color too
|
||||
const dateIsValid = value && value.date instanceof Date;
|
||||
// Error message and input error styles are only shown if the
|
||||
// field has been touched and the validation has failed.
|
||||
const hasError = touched && invalid && error;
|
||||
|
||||
const inputClasses = classNames({
|
||||
[css.pickerSuccess]: dateIsValid,
|
||||
[css.pickerError]: hasError,
|
||||
});
|
||||
|
||||
const { onBlur, onFocus, ...restOfInput } = input;
|
||||
const inputProps = {
|
||||
onBlur: input.onBlur,
|
||||
onFocus: input.onFocus,
|
||||
useMobileMargins,
|
||||
...restOfInput,
|
||||
...rest,
|
||||
};
|
||||
const classes = classNames(rootClassName || css.fieldRoot, className);
|
||||
const errorClasses = classNames({ [css.mobileMargins]: useMobileMargins });
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{label ? (
|
||||
<label className={classNames({ [css.mobileMargins]: useMobileMargins })} htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
) : null}
|
||||
<DateInput className={inputClasses} {...inputProps} />
|
||||
<ValidationError className={errorClasses} fieldMeta={meta} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FieldDateInputComponent.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
useMobileMargins: false,
|
||||
id: null,
|
||||
label: null,
|
||||
placeholderText: null,
|
||||
};
|
||||
|
||||
FieldDateInputComponent.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
useMobileMargins: bool,
|
||||
id: string,
|
||||
label: string,
|
||||
placeholderText: string,
|
||||
input: object.isRequired,
|
||||
meta: object.isRequired,
|
||||
};
|
||||
|
||||
const FieldDateInput = props => {
|
||||
return <Field component={FieldDateInputComponent} {...props} />;
|
||||
};
|
||||
|
||||
export { DateInput };
|
||||
export default FieldDateInput;
|
||||
22
src/components/FieldDateInput/FieldDateInput.test.js
Normal file
22
src/components/FieldDateInput/FieldDateInput.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import { renderDeep } from '../../util/test-helpers';
|
||||
import { DateInput } from './FieldDateInput';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('DateInput', () => {
|
||||
it('matches snapshot', () => {
|
||||
const props = {
|
||||
name: 'bookingDate',
|
||||
onBlur: noop,
|
||||
onChange: noop,
|
||||
onFocus: noop,
|
||||
onDragStart: noop,
|
||||
onDrop: noop,
|
||||
id: 'bookingDate',
|
||||
placeholderText: 'today',
|
||||
};
|
||||
const tree = renderDeep(<DateInput {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
39
src/components/FieldDateInput/NextMonthIcon.js
Normal file
39
src/components/FieldDateInput/NextMonthIcon.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import css from './DateInput.css';
|
||||
|
||||
const NextMonthIcon = props => {
|
||||
const { className, rootClassName } = props;
|
||||
const classes = classNames(rootClassName || css.rootNextMonthIcon, className);
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
width="9"
|
||||
height="13"
|
||||
viewBox="0 0 9 13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.472 6.97c.26-.26.26-.68 0-.94L2.14.694c-.263-.26-.684-.26-.944 0-.26.26-.26.683 0 .943L6.056 6.5l-4.86 4.862c-.26.26-.26.683 0 .943.26.26.68.26.943 0L7.47 6.97z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
NextMonthIcon.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
};
|
||||
|
||||
NextMonthIcon.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
};
|
||||
|
||||
export default NextMonthIcon;
|
||||
39
src/components/FieldDateInput/PreviousMonthIcon.js
Normal file
39
src/components/FieldDateInput/PreviousMonthIcon.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import css from './DateInput.css';
|
||||
|
||||
const PreviousMonthIcon = props => {
|
||||
const { className, rootClassName } = props;
|
||||
const classes = classNames(rootClassName || css.rootPreviousMonthIcon, className);
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
width="9"
|
||||
height="13"
|
||||
viewBox="0 0 9 13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.195 6.03c-.26.26-.26.68 0 .94l5.333 5.335c.262.26.683.26.943 0 .262-.26.262-.683 0-.943L2.61 6.5l4.86-4.862c.262-.26.262-.683 0-.943-.26-.26-.68-.26-.942 0L1.195 6.03z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
PreviousMonthIcon.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
};
|
||||
|
||||
PreviousMonthIcon.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
};
|
||||
|
||||
export default PreviousMonthIcon;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DateInput matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<div
|
||||
className="SingleDatePicker SingleDatePicker_1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="SingleDatePickerInput SingleDatePickerInput_1"
|
||||
>
|
||||
<div
|
||||
className="DateInput DateInput_1"
|
||||
>
|
||||
<input
|
||||
aria-describedby="DateInput__screen-reader-message-bookingDate"
|
||||
aria-label="today"
|
||||
autoComplete="off"
|
||||
className="DateInput_input DateInput_input_1"
|
||||
disabled={false}
|
||||
id="bookingDate"
|
||||
name="bookingDate"
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="today"
|
||||
readOnly={false}
|
||||
required={false}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<p
|
||||
className="DateInput_screenReaderMessage DateInput_screenReaderMessage_1"
|
||||
id="DateInput__screen-reader-message-bookingDate"
|
||||
>
|
||||
FieldDateInput.screenReaderInputMessage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -33,6 +33,7 @@ export { default as FilterPanel } from './FilterPanel/FilterPanel';
|
|||
export { default as FieldBirthdayInput } from './FieldBirthdayInput/FieldBirthdayInput';
|
||||
export { default as FieldCheckbox } from './FieldCheckbox/FieldCheckbox';
|
||||
export { default as FieldCurrencyInput } from './FieldCurrencyInput/FieldCurrencyInput';
|
||||
export { default as FieldDateInput } from './FieldDateInput/FieldDateInput';
|
||||
export { default as FieldDateRangeInput } from './FieldDateRangeInput/FieldDateRangeInput';
|
||||
export { default as FieldGroupCheckbox } from './FieldGroupCheckbox/FieldGroupCheckbox';
|
||||
export { default as FieldReviewRating } from './FieldReviewRating/FieldReviewRating';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import * as ExpandingTextarea from './components/ExpandingTextarea/ExpandingText
|
|||
import * as FieldBirthdayInput from './components/FieldBirthdayInput/FieldBirthdayInput.example';
|
||||
import * as FieldCheckbox from './components/FieldCheckbox/FieldCheckbox.example';
|
||||
import * as FieldCurrencyInput from './components/FieldCurrencyInput/FieldCurrencyInput.example';
|
||||
import * as FieldDateInput from './components/FieldDateInput/FieldDateInput.example';
|
||||
import * as FieldDateRangeInput from './components/FieldDateRangeInput/FieldDateRangeInput.example';
|
||||
import * as FieldGroupCheckbox from './components/FieldGroupCheckbox/FieldGroupCheckbox.example';
|
||||
import * as FieldReviewRating from './components/FieldReviewRating/FieldReviewRating.example';
|
||||
|
|
@ -93,6 +94,7 @@ export {
|
|||
FieldBirthdayInput,
|
||||
FieldCheckbox,
|
||||
FieldCurrencyInput,
|
||||
FieldDateInput,
|
||||
FieldDateRangeInput,
|
||||
FieldGroupCheckbox,
|
||||
FieldReviewRating,
|
||||
|
|
|
|||
|
|
@ -192,6 +192,11 @@
|
|||
"EnquiryForm.messageRequired": "A message is required",
|
||||
"EnquiryForm.sendEnquiryError": "Whoops, something went wrong. Please try again.",
|
||||
"EnquiryForm.submitButtonText": "Send enquiry",
|
||||
"FieldDateInput.clearDate": "Clear Date",
|
||||
"FieldDateInput.closeDatePicker": "Close",
|
||||
"FieldDateInput.invalidDate": "The start date is not valid",
|
||||
"FieldDateInput.screenReaderInputMessage": "Date input",
|
||||
"FieldDateInput.placeholderText": "Add start date",
|
||||
"FieldDateRangeInput.clearDate": "Clear Date",
|
||||
"FieldDateRangeInput.closeDatePicker": "Close",
|
||||
"FieldDateRangeInput.endDatePlaceholderText": "Add end date",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ export const autocompletePlaceSelected = message => value => {
|
|||
return selectedPlaceIsValid ? VALID : message;
|
||||
};
|
||||
|
||||
export const bookingDateRequired = inValidDateMessage => value => {
|
||||
const dateIsValid = value && value.date instanceof Date;
|
||||
return !dateIsValid ? inValidDateMessage : VALID;
|
||||
};
|
||||
|
||||
export const bookingDatesRequired = (inValidStartDateMessage, inValidEndDateMessage) => value => {
|
||||
const startDateIsValid = value && value.startDate instanceof Date;
|
||||
const endDateIsValid = value && value.endDate instanceof Date;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue