Skip to content

Commit

Permalink
fix: min max validation without native input[type=time] support (#273)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixelbandito authored Oct 19, 2020
1 parent f766d63 commit 89708d3
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 17 deletions.
38 changes: 27 additions & 11 deletions src/components/InputTime/AsString.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -109,15 +110,30 @@ const AsString: React.FC<AsStringProps> = ({
value ? getLocaleTimeStringFromShortTimeString(value, { formatTime }) : ''
);

const syncValidity = (
refFrom?: RefObject<HTMLInputElement>,
refTo?: RefObject<HTMLInputElement>
) => {
if (refTo?.current && refFrom?.current) {
refTo.current.setCustomValidity(refFrom.current.validationMessage);
setValidity(!refFrom.current.validationMessage);
}
};
const syncValidity = useCallback(
(
refFrom?: RefObject<HTMLInputElement>,
refTo?: RefObject<HTMLInputElement>
) => {
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) => {
Expand Down Expand Up @@ -188,12 +204,12 @@ const AsString: React.FC<AsStringProps> = ({
}

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 (
<div
Expand Down
86 changes: 80 additions & 6 deletions src/components/InputTime/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import React from 'react';
import { render } from '@testing-library/react';

import {
handleDispatchNativeInputChange,
guessTimeFromString,
getStartOfDay,
getShortTimeString,
getLocaleTimeStringFromShortTimeString,
getEndOfDay,
getDateTimeFromShortTimeString,
getEndOfDay,
getInputTimeMinMaxValidationMessagePolyfill,
getLocaleTimeStringFromShortTimeString,
getShortTimeString,
getStartOfDay,
guessTimeFromString,
handleDispatchNativeInputChange,
} from './utils';

const getFormattedTimeWithTimeZone = ({
Expand Down Expand Up @@ -313,3 +314,76 @@ describe('guessTimeFromString', () => {
);
});
});

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();
});
});
31 changes: 31 additions & 0 deletions src/components/InputTime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

0 comments on commit 89708d3

Please sign in to comment.