@wordpress/components
Version:
UI components for WordPress.
290 lines (264 loc) • 7.97 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
import { isInteger } from 'lodash';
import moment from 'moment';
/**
* WordPress dependencies
*/
import {
createElement,
useState,
useMemo,
useEffect,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import Button from '../button';
import ButtonGroup from '../button-group';
import TimeZone from './timezone';
/**
* Module Constants
*/
const TIMEZONELESS_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
/**
* <UpdateOnBlurAsIntegerField>
* A shared component to parse, validate, and handle remounting of the underlying form field element like <input> and <select>.
*
* @param {Object} props Component props.
* @param {string} props.as Render the component as specific element tag, defaults to "input".
* @param {number|string} props.value The default value of the component which will be parsed to integer.
* @param {Function} props.onUpdate Call back when blurred and validated.
*/
function UpdateOnBlurAsIntegerField( { as, value, onUpdate, ...props } ) {
function handleBlur( event ) {
const { target } = event;
if ( value === target.value ) {
return;
}
const parsedValue = parseInt( target.value, 10 );
// Run basic number validation on the input.
if (
! isInteger( parsedValue ) ||
( typeof props.max !== 'undefined' && parsedValue > props.max ) ||
( typeof props.min !== 'undefined' && parsedValue < props.min )
) {
// If validation failed, reset the value to the previous valid value.
target.value = value;
} else {
// Otherwise, it's valid, call onUpdate.
onUpdate( target.name, parsedValue );
}
}
return createElement( as || 'input', {
// Re-mount the input value to accept the latest value as the defaultValue.
key: value,
defaultValue: value,
onBlur: handleBlur,
...props,
} );
}
/**
* <TimePicker>
*
* @typedef {Date|string|number} WPValidDateTimeFormat
*
* @param {Object} props Component props.
* @param {boolean} props.is12Hour Should the time picker showed in 12 hour format or 24 hour format.
* @param {WPValidDateTimeFormat} props.currentTime The initial current time the time picker should render.
* @param {Function} props.onChange Callback function when the date changed.
*/
export function TimePicker( { is12Hour, currentTime, onChange } ) {
const [ date, setDate ] = useState( () =>
// Truncate the date at the minutes, see: #15495.
moment( currentTime ).startOf( 'minutes' )
);
// Reset the state when currentTime changed.
useEffect( () => {
setDate(
currentTime ? moment( currentTime ).startOf( 'minutes' ) : moment()
);
}, [ currentTime ] );
const { day, month, year, minutes, hours, am } = useMemo(
() => ( {
day: date.format( 'DD' ),
month: date.format( 'MM' ),
year: date.format( 'YYYY' ),
minutes: date.format( 'mm' ),
hours: date.format( is12Hour ? 'hh' : 'HH' ),
am: date.format( 'H' ) <= 11 ? 'AM' : 'PM',
} ),
[ date, is12Hour ]
);
/**
* Function that sets the date state and calls the onChange with a new date.
* The date is truncated at the minutes.
*
* @param {Object} newDate The date object.
*/
function changeDate( newDate ) {
setDate( newDate );
onChange( newDate.format( TIMEZONELESS_FORMAT ) );
}
function update( name, value ) {
// Clone the date and call the specific setter function according to `name`.
const newDate = date.clone()[ name ]( value );
changeDate( newDate );
}
function updateAmPm( value ) {
return () => {
if ( am === value ) {
return;
}
const parsedHours = parseInt( hours, 10 );
const newDate = date
.clone()
.hours(
value === 'PM'
? ( ( parsedHours % 12 ) + 12 ) % 24
: parsedHours % 12
);
changeDate( newDate );
};
}
const dayFormat = (
<div className="components-datetime__time-field components-datetime__time-field-day">
<UpdateOnBlurAsIntegerField
aria-label={ __( 'Day' ) }
className="components-datetime__time-field-day-input"
type="number"
// The correct function to call in moment.js is "date" not "day".
name="date"
value={ day }
step={ 1 }
min={ 1 }
max={ 31 }
onUpdate={ update }
/>
</div>
);
const monthFormat = (
<div className="components-datetime__time-field components-datetime__time-field-month">
<UpdateOnBlurAsIntegerField
as="select"
aria-label={ __( 'Month' ) }
className="components-datetime__time-field-month-select"
name="month"
value={ month }
// The value starts from 0, so we have to -1 when setting month.
onUpdate={ ( key, value ) => update( key, value - 1 ) }
>
<option value="01">{ __( 'January' ) }</option>
<option value="02">{ __( 'February' ) }</option>
<option value="03">{ __( 'March' ) }</option>
<option value="04">{ __( 'April' ) }</option>
<option value="05">{ __( 'May' ) }</option>
<option value="06">{ __( 'June' ) }</option>
<option value="07">{ __( 'July' ) }</option>
<option value="08">{ __( 'August' ) }</option>
<option value="09">{ __( 'September' ) }</option>
<option value="10">{ __( 'October' ) }</option>
<option value="11">{ __( 'November' ) }</option>
<option value="12">{ __( 'December' ) }</option>
</UpdateOnBlurAsIntegerField>
</div>
);
const dayMonthFormat = is12Hour ? (
<>
{ dayFormat }
{ monthFormat }
</>
) : (
<>
{ monthFormat }
{ dayFormat }
</>
);
return (
<div className={ classnames( 'components-datetime__time' ) }>
<fieldset>
<legend className="components-datetime__time-legend invisible">
{ __( 'Date' ) }
</legend>
<div className="components-datetime__time-wrapper">
{ dayMonthFormat }
<div className="components-datetime__time-field components-datetime__time-field-year">
<UpdateOnBlurAsIntegerField
aria-label={ __( 'Year' ) }
className="components-datetime__time-field-year-input"
type="number"
name="year"
step={ 1 }
min={ 0 }
max={ 9999 }
value={ year }
onUpdate={ update }
/>
</div>
</div>
</fieldset>
<fieldset>
<legend className="components-datetime__time-legend invisible">
{ __( 'Time' ) }
</legend>
<div className="components-datetime__time-wrapper">
<div className="components-datetime__time-field components-datetime__time-field-time">
<UpdateOnBlurAsIntegerField
aria-label={ __( 'Hours' ) }
className="components-datetime__time-field-hours-input"
type="number"
name="hours"
step={ 1 }
min={ is12Hour ? 1 : 0 }
max={ is12Hour ? 12 : 23 }
value={ hours }
onUpdate={ update }
/>
<span
className="components-datetime__time-separator"
aria-hidden="true"
>
:
</span>
<UpdateOnBlurAsIntegerField
aria-label={ __( 'Minutes' ) }
className="components-datetime__time-field-minutes-input"
type="number"
name="minutes"
step={ 1 }
min={ 0 }
max={ 59 }
value={ minutes }
onUpdate={ update }
/>
</div>
{ is12Hour && (
<ButtonGroup className="components-datetime__time-field components-datetime__time-field-am-pm">
<Button
isPrimary={ am === 'AM' }
isSecondary={ am !== 'AM' }
onClick={ updateAmPm( 'AM' ) }
className="components-datetime__time-am-button"
>
{ __( 'AM' ) }
</Button>
<Button
isPrimary={ am === 'PM' }
isSecondary={ am !== 'PM' }
onClick={ updateAmPm( 'PM' ) }
className="components-datetime__time-pm-button"
>
{ __( 'PM' ) }
</Button>
</ButtonGroup>
) }
<TimeZone />
</div>
</fieldset>
</div>
);
}
export default TimePicker;