diff --git a/src/components/InputTime/AsString.tsx b/src/components/InputTime/AsString.tsx index e017a2aa..3fe0ff8f 100644 --- a/src/components/InputTime/AsString.tsx +++ b/src/components/InputTime/AsString.tsx @@ -17,6 +17,7 @@ import inputStyles from '../Input/Input.module.css'; import { SECONDS_PER_DAY, SECONDS_PER_MINUTE } from './constants'; import { + getInputTimeMinMaxValidationMessagePolyfill, getLocaleTimeStringFromShortTimeString, getShortTimeString, getStartOfDay, @@ -109,15 +110,30 @@ const AsString: React.FC = ({ value ? getLocaleTimeStringFromShortTimeString(value, { formatTime }) : '' ); - const syncValidity = ( - refFrom?: RefObject, - refTo?: RefObject - ) => { - if (refTo?.current && refFrom?.current) { - refTo.current.setCustomValidity(refFrom.current.validationMessage); - setValidity(!refFrom.current.validationMessage); - } - }; + const syncValidity = useCallback( + ( + refFrom?: RefObject, + refTo?: RefObject + ) => { + if (refTo?.current && refFrom?.current) { + let { validationMessage } = refFrom.current; + + // Safari and IE don't support time inputs natively + // Mostly not a problem, but they need special handling for min/max validation + if (refFrom.current.type !== 'time') { + validationMessage = getInputTimeMinMaxValidationMessagePolyfill({ + value: refFrom.current.value, + max, + min, + }); + } + + refTo.current.setCustomValidity(validationMessage); + setValidity(!validationMessage); + } + }, + [max, min] + ); const changeShadowTimeInput = useCallback( (nextValue?: string) => { @@ -188,12 +204,12 @@ const AsString: React.FC = ({ } syncValidity(shadowTimeInputRef, localRef); - }, [formatTime, localRef, shadowTimeInputRef, step, value]); + }, [formatTime, localRef, shadowTimeInputRef, step, syncValidity, value]); // Sync validity on min/max changes useEffect(() => { syncValidity(shadowTimeInputRef, localRef); - }, [localRef, max, min, shadowTimeInputRef]); + }, [localRef, max, min, shadowTimeInputRef, syncValidity]); return (
{ ); }); }); + +describe('getInputTimeMinMaxValidationMessagePolyfill', () => { + it('handles validation with `max` and `min`', () => { + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + max: '20:00', + min: '10:00', + value: '15:00', + }) + ).toBeFalsy(); + + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + max: '20:00', + min: '10:00', + value: '09:59', + }) + ).toBeTruthy(); + + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + max: '20:00', + min: '10:00', + value: '20:01', + }) + ).toBeTruthy(); + }); + + it('handles validation with `max` only', () => { + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + max: '10:00', + value: '10:00', + }) + ).toBeFalsy(); + + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + max: '10:00', + value: '10:01', + }) + ).toBeTruthy(); + }); + + it('handles validation with `min` only', () => { + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + min: '10:00', + value: '10:00', + }) + ).toBeFalsy(); + + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + min: '10:00', + value: '09:59', + }) + ).toBeTruthy(); + }); + + it('handles validation without a `value`', () => { + expect( + getInputTimeMinMaxValidationMessagePolyfill({ + max: '10:00', + min: '10:00', + }) + ).toBeFalsy(); + }); + + it('handles validation without anything', () => { + expect(getInputTimeMinMaxValidationMessagePolyfill({})).toBeFalsy(); + }); +}); diff --git a/src/components/InputTime/utils.ts b/src/components/InputTime/utils.ts index 5af8ea9c..37546d97 100644 --- a/src/components/InputTime/utils.ts +++ b/src/components/InputTime/utils.ts @@ -152,3 +152,34 @@ export const guessTimeFromString = (string: string) => { const time = getShortTimeString(hours, minutes); return hasValidUserInput ? { time, hours, minutes } : {}; }; + +const getTotalMinutes = (timeString?: string) => { + const [hours, minutes] = (timeString || ':').split(':'); + const totalMinutes = parseInt(hours, 10) * 60 + parseInt(minutes, 10); + return !Number.isNaN(totalMinutes) ? totalMinutes : undefined; +}; + +export const getInputTimeMinMaxValidationMessagePolyfill = ({ + max, + min, + value, +}: { + max?: string; + min?: string; + value?: string; +}) => { + let validationMessage = ''; + const totalMinutes = getTotalMinutes(value || ':'); + const maxTotalMinutes = getTotalMinutes(max); + const minTotalMinutes = getTotalMinutes(min); + + if (totalMinutes && maxTotalMinutes && totalMinutes > maxTotalMinutes) { + validationMessage = `Value must be ${max} or earlier.`; + } + + if (totalMinutes && minTotalMinutes && totalMinutes < minTotalMinutes) { + validationMessage = `Value must be ${min} or later.`; + } + + return validationMessage; +};