diff --git a/packages/react-core/src/components/TimePicker/TimePicker.tsx b/packages/react-core/src/components/TimePicker/TimePicker.tsx index e08e5dec804..ab085b107e2 100644 --- a/packages/react-core/src/components/TimePicker/TimePicker.tsx +++ b/packages/react-core/src/components/TimePicker/TimePicker.tsx @@ -9,7 +9,16 @@ import { TimeOption } from './TimeOption'; import { KeyTypes, SelectDirection } from '../Select'; import { InputGroup } from '../InputGroup'; import { TextInput, TextInputProps } from '../TextInput'; -import { parseTime, validateTime, makeTimeOptions, amSuffix, pmSuffix, getHours, getMinutes } from './TimePickerUtils'; +import { + parseTime, + validateTime, + makeTimeOptions, + amSuffix, + pmSuffix, + getHours, + getMinutes, + isWithinMinMax +} from './TimePickerUtils'; export interface TimePickerProps extends Omit, 'onChange' | 'onFocus' | 'onBlur' | 'disabled' | 'ref'> { @@ -27,6 +36,8 @@ export interface TimePickerProps time?: string | Date; /** Error message to display when the time is provided in an invalid format. */ invalidFormatErrorMessage?: string; + /** Error message to display when the time provided is not within the minTime/maxTime constriants */ + invalidMinMaxErrorMessage?: string; /** True if the time is 24 hour time. False if the time is 12 hour time */ is24Hour?: boolean; /** Optional event handler called each time the value in the time picker input changes. */ @@ -50,6 +61,10 @@ export interface TimePickerProps stepMinutes?: number; /** Additional props for input field */ inputProps?: TextInputProps; + /** A time string indicating the minimum value allowed. The format could be an ISO 8601 formatted date string or in 'HH{delimiter}MM' format */ + minTime?: string | Date; + /** A time string indicating the maximum value allowed. The format could be an ISO 8601 formatted date string or in 'HH{delimiter}MM' format */ + maxTime?: string | Date; } interface TimePickerState { @@ -59,6 +74,8 @@ interface TimePickerState { focusedIndex: number; scrollIndex: number; timeRegex: RegExp; + minTimeState: string; + maxTimeState: string; } export class TimePicker extends React.Component { @@ -74,6 +91,7 @@ export class TimePicker extends React.Component (this.menuRef && this.menuRef.current ? Array.from(this.menuRef.current.children) : []) as HTMLElement[]; - isValid = (time: string) => { + isValidFormat = (time: string) => { if (this.props.validateTime) { return this.props.validateTime(time); } @@ -261,6 +288,14 @@ export class TimePicker extends React.Component { + const { delimiter } = this.props; + const { minTimeState, maxTimeState } = this.state; + return isWithinMinMax(minTimeState, maxTimeState, time, delimiter); + }; + + isValid = (time: string) => this.isValidFormat(time) && this.isValidTime(time); + onToggle = (isOpen: boolean) => { // on close, parse and validate input this.setState(prevState => { @@ -329,6 +364,7 @@ export class TimePicker extends React.Component {isInvalid && (
- {invalidFormatErrorMessage} + {!isValidFormat ? invalidFormatErrorMessage : invalidMinMaxErrorMessage}
)} diff --git a/packages/react-core/src/components/TimePicker/TimePickerUtils.tsx b/packages/react-core/src/components/TimePicker/TimePickerUtils.tsx index 00e521aeb25..46510b446bb 100644 --- a/packages/react-core/src/components/TimePicker/TimePickerUtils.tsx +++ b/packages/react-core/src/components/TimePicker/TimePickerUtils.tsx @@ -1,12 +1,18 @@ export const amSuffix = ' AM'; export const pmSuffix = ' PM'; -export const makeTimeOptions = (stepMinutes: number, hour12: boolean, delimiter: string) => { +export const makeTimeOptions = ( + stepMinutes: number, + hour12: boolean, + delimiter: string, + minTime: string, + maxTime: string +) => { const res = []; const iter = new Date(new Date().setHours(0, 0, 0, 0)); const iterDay = iter.getDay(); while (iter.getDay() === iterDay) { - let hour = iter.getHours(); + let hour: string | number = iter.getHours(); let suffix = amSuffix; if (hour12) { if (hour === 0) { @@ -18,19 +24,18 @@ export const makeTimeOptions = (stepMinutes: number, hour12: boolean, delimiter: hour %= 12; } } - res.push( - (hour12 ? hour.toString() : hour.toString().padStart(2, '0')) + - delimiter + - iter - .getMinutes() - .toString() - .padStart(2, '0') + - (hour12 ? suffix : '') - ); - + hour = hour12 ? hour.toString() : hour.toString().padStart(2, '0'); + const minutes = iter + .getMinutes() + .toString() + .padStart(2, '0'); + const timeOption = hour + delimiter + minutes + (hour12 ? suffix : ''); + // time option is valid if within min/max constraints + if (isWithinMinMax(minTime, maxTime, timeOption, delimiter)) { + res.push(timeOption); + } iter.setMinutes(iter.getMinutes() + stepMinutes); } - return res; }; @@ -53,26 +58,20 @@ export const parseTime = (time: string | Date, timeRegex: RegExp, delimiter: str } else if (typeof time === 'string') { time = time.trim(); if (is12Hour && time !== '' && validateTime(time, timeRegex, delimiter, is12Hour)) { + const [, hours, minutes, suffix = ''] = timeRegex.exec(time); + const uppercaseSuffix = suffix.toUpperCase(); // Format AM/PM according to design let ampm = ''; - if (time.toUpperCase().includes(amSuffix.toUpperCase().trim())) { - time = time - .toUpperCase() - .replace(amSuffix.toUpperCase().trim(), '') - .trim(); + if (uppercaseSuffix === amSuffix.toUpperCase().trim()) { ampm = amSuffix; - } else if (time.toUpperCase().includes(pmSuffix.toUpperCase().trim())) { - time = time - .toUpperCase() - .replace(pmSuffix.toUpperCase().trim(), '') - .trim(); + } else if (uppercaseSuffix === pmSuffix.toUpperCase().trim()) { ampm = pmSuffix; } else { // if this 12 hour time is missing am/pm but otherwise valid, // append am/pm depending on time of day ampm = new Date().getHours() > 11 ? pmSuffix : amSuffix; } - return `${time}${ampm}`; + return `${hours}${delimiter}${minutes}${ampm}`; } } return time.toString(); @@ -111,3 +110,34 @@ export const getMinutes = (time: string, timeRegex: RegExp) => { const parts = time.match(timeRegex); return parts && parts.length ? parseInt(parts[2]) : null; }; + +export const isWithinMinMax = (minTime: string, maxTime: string, time: string, delimiter: string) => { + // do not throw error if empty string + if (time.trim() === '') { + return true; + } + const convertTo24Hour = (time: string): string => { + const timeReg = new RegExp(`^\\s*(\\d\\d?)${delimiter}([0-5]\\d)\\s*([AaPp][Mm])?\\s*$`); + const regMatches = timeReg.exec(time); + if (!regMatches || !regMatches.length) { + return; + } + let hours = regMatches[1].padStart(2, '0'); + const minutes = regMatches[2]; + const suffix = regMatches[3] || ''; + if (suffix.toUpperCase() === 'PM' && hours !== '12') { + hours = `${parseInt(hours) + 12}`; + } else if (suffix.toUpperCase() === 'AM' && hours === '12') { + hours = '00'; + } + return `${hours}${delimiter}${minutes}`; + }; + + // correctly format as 24hr times (12:30AM => 00:30, 1:15 => 01:15) + minTime = convertTo24Hour(minTime); + time = convertTo24Hour(time); + maxTime = convertTo24Hour(maxTime); + + // simple string comparison for 24hr times + return minTime <= time && time <= maxTime; +}; diff --git a/packages/react-core/src/components/TimePicker/examples/TimePicker.md b/packages/react-core/src/components/TimePicker/examples/TimePicker.md index aa573045f12..a8e713d014a 100644 --- a/packages/react-core/src/components/TimePicker/examples/TimePicker.md +++ b/packages/react-core/src/components/TimePicker/examples/TimePicker.md @@ -41,3 +41,14 @@ import { TimePicker } from '@patternfly/react-core'; ``` + +### Minimum/maximum times + +The `minTime`/`maxTime` props restrict the options shown for the user to select from as well as trigger the `invalidMinMaxErrorMessage` if the user enters a time outside this range. + +```js +import React from 'react'; +import { TimePicker } from '@patternfly/react-core'; + + +```