Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date/DateTime inputs #756

Merged
merged 19 commits into from Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 8 additions & 52 deletions packages/components/src/DateInput.tsx
@@ -1,10 +1,8 @@
import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import Log from '@deephaven/log';
import type { SelectionSegment } from './MaskedInput';
import MaskedInput, {
DEFAULT_GET_PREFERRED_REPLACEMENT_STRING,
} from './MaskedInput';
import MaskedInput, { SelectionSegment } from './MaskedInput';
import { getNextSegmentValue } from './DateInputUtils';

const log = Log.module('DateInput');

Expand All @@ -21,56 +19,19 @@ type DateInputProps = {
'data-testid'?: string;
};

// Forward ref causes a false positive for display-name in eslint:
// https://github.com/yannickcr/eslint-plugin-react/issues/2269
// eslint-disable-next-line react/display-name
const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
(props: DateInputProps, ref) => {
const {
className = '',
onChange = () => false,
onChange = () => undefined,
defaultValue = '',
onFocus = () => false,
onBlur = () => false,
onFocus = () => undefined,
onBlur = () => undefined,
'data-testid': dataTestId,
} = props;
const [value, setValue] = useState(defaultValue);
const [selection, setSelection] = useState<SelectionSegment>();

function getNextNumberSegmentValue(
delta: number,
segmentValue: string,
lowerBound: number,
upperBound: number,
length: number
) {
const modValue = upperBound - lowerBound + 1;
const newSegmentValue =
((((parseInt(segmentValue, 10) - delta - lowerBound) % modValue) +
modValue) %
modValue) +
lowerBound;
return `${newSegmentValue}`.padStart(length, '0');
}

function getNextSegmentValue(
range: SelectionSegment,
delta: number,
segmentValue: string
): string {
const { selectionStart } = range;
if (selectionStart === 0) {
return getNextNumberSegmentValue(delta, segmentValue, 1900, 2099, 4);
}
if (selectionStart === 5) {
return getNextNumberSegmentValue(delta, segmentValue, 1, 12, 2);
}
if (selectionStart === 8) {
return getNextNumberSegmentValue(delta, segmentValue, 1, 31, 2);
}
return '';
}

const handleChange = useCallback(
(newValue: string): void => {
log.debug('handleChange', newValue);
Expand All @@ -80,22 +41,15 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
[onChange]
);

function handleSelect(newSelection: SelectionSegment) {
setSelection(newSelection);
}

return (
<div className="d-flex flex-row align-items-center">
<MaskedInput
ref={ref}
className={classNames(className)}
example={EXAMPLES}
getNextSegmentValue={getNextSegmentValue}
getPreferredReplacementString={
DEFAULT_GET_PREFERRED_REPLACEMENT_STRING
}
onChange={handleChange}
onSelect={handleSelect}
onSelect={setSelection}
pattern={DATE_PATTERN}
placeholder={DATE_FORMAT}
selection={selection}
Expand All @@ -109,6 +63,8 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
}
);

vbabich marked this conversation as resolved.
Show resolved Hide resolved
DateInput.displayName = 'DateInput';

DateInput.defaultProps = {
className: '',
onChange: () => false,
Expand Down
48 changes: 48 additions & 0 deletions packages/components/src/DateInputUtils.ts
@@ -0,0 +1,48 @@
import { SelectionSegment } from './MaskedInput';

export function getNextNumberSegmentValue(
delta: number,
segmentValue: string,
lowerBound: number,
upperBound: number,
length: number
): string {
const modValue = upperBound - lowerBound + 1;
const newSegmentValue =
((((parseInt(segmentValue, 10) - delta - lowerBound) % modValue) +
modValue) %
modValue) +
lowerBound;
return `${newSegmentValue}`.padStart(length, '0');
}

export function getNextSegmentValue(
range: SelectionSegment,
delta: number,
segmentValue: string
): string {
const { selectionStart } = range;
if (selectionStart === 0) {
return getNextNumberSegmentValue(delta, segmentValue, 1900, 2099, 4);
}
if (selectionStart === 5) {
return getNextNumberSegmentValue(delta, segmentValue, 1, 12, 2);
}
if (selectionStart === 8) {
return getNextNumberSegmentValue(delta, segmentValue, 1, 31, 2);
}
if (selectionStart === 11) {
// Hours input
return getNextNumberSegmentValue(delta, segmentValue, 0, 23, 2);
}
if (selectionStart === 17 || selectionStart === 14) {
// Minutes/seconds input
return getNextNumberSegmentValue(delta, segmentValue, 0, 59, 2);
}
if (selectionStart === 20 || selectionStart === 24 || selectionStart === 28) {
// Milli, micro, and nanosecond input
return getNextNumberSegmentValue(delta, segmentValue, 0, 999, 3);
}

return segmentValue;
}
85 changes: 11 additions & 74 deletions packages/components/src/DateTimeInput.tsx
@@ -1,10 +1,8 @@
import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import Log from '@deephaven/log';
import type { SelectionSegment } from './MaskedInput';
import MaskedInput, {
DEFAULT_GET_PREFERRED_REPLACEMENT_STRING,
} from './MaskedInput';
import MaskedInput, { SelectionSegment } from './MaskedInput';
import { getNextSegmentValue } from './DateInputUtils';

const log = Log.module('DateTimeInput');

Expand Down Expand Up @@ -38,77 +36,21 @@ const removeSeparators = (value: string) => value.replace(/\u200B/g, '');

const EXAMPLES = [addSeparators(DEFAULT_VALUE_STRING)];

// Forward ref causes a false positive for display-name in eslint:
// https://github.com/yannickcr/eslint-plugin-react/issues/2269
// eslint-disable-next-line react/display-name
const DateTimeInput = React.forwardRef<HTMLInputElement, DateTimeInputProps>(
(props: DateTimeInputProps, ref) => {
const {
className = '',
onChange = () => false,
onChange = () => undefined,
defaultValue = '',
onFocus = () => false,
onBlur = () => false,
onFocus = () => undefined,
onBlur = () => undefined,
'data-testid': dataTestId,
} = props;
const [value, setValue] = useState(
defaultValue.length > 0 ? addSeparators(defaultValue) : ''
);
const [selection, setSelection] = useState<SelectionSegment>();

function getNextNumberSegmentValue(
delta: number,
segmentValue: string,
lowerBound: number,
upperBound: number,
length: number
) {
const modValue = upperBound - lowerBound + 1;
const newSegmentValue =
((((parseInt(segmentValue, 10) - delta - lowerBound) % modValue) +
modValue) %
modValue) +
lowerBound;
const result = `${newSegmentValue}`.padStart(length, '0');
log.debug('getNextNumberSegmentValue', modValue, newSegmentValue, result);
return result;
}

function getNextSegmentValue(
range: SelectionSegment,
delta: number,
segmentValue: string
): string {
const { selectionStart } = range;
if (selectionStart === 0) {
return getNextNumberSegmentValue(delta, segmentValue, 1900, 2099, 4);
}
if (selectionStart === 5) {
return getNextNumberSegmentValue(delta, segmentValue, 1, 12, 2);
}
if (selectionStart === 8) {
return getNextNumberSegmentValue(delta, segmentValue, 1, 31, 2);
}
if (selectionStart === 11) {
// Hours input
return getNextNumberSegmentValue(delta, segmentValue, 0, 23, 2);
}
if (selectionStart === 17 || selectionStart === 14) {
// Minutes/seconds input
return getNextNumberSegmentValue(delta, segmentValue, 0, 59, 2);
}
if (
selectionStart === 20 ||
selectionStart === 24 ||
selectionStart === 28
) {
// Milli, micro, and nanosecond input
return getNextNumberSegmentValue(delta, segmentValue, 0, 999, 3);
}

return segmentValue;
}

const handleChange = useCallback(
(newValue: string): void => {
log.debug('handleChange', newValue);
Expand All @@ -118,22 +60,15 @@ const DateTimeInput = React.forwardRef<HTMLInputElement, DateTimeInputProps>(
[onChange]
);

function handleSelect(newSelection: SelectionSegment) {
setSelection(newSelection);
}

return (
<div className="d-flex flex-row align-items-center">
<MaskedInput
ref={ref}
className={classNames(className)}
example={EXAMPLES}
getNextSegmentValue={getNextSegmentValue}
getPreferredReplacementString={
DEFAULT_GET_PREFERRED_REPLACEMENT_STRING
}
onChange={handleChange}
onSelect={handleSelect}
onSelect={setSelection}
pattern={FULL_DATE_PATTERN}
placeholder={FULL_DATE_FORMAT}
selection={selection}
Expand All @@ -147,12 +82,14 @@ const DateTimeInput = React.forwardRef<HTMLInputElement, DateTimeInputProps>(
}
);

DateTimeInput.displayName = 'DateTimeInput';

DateTimeInput.defaultProps = {
className: '',
onChange: () => false,
onChange: () => undefined,
defaultValue: '',
onFocus: () => false,
onBlur: () => false,
onFocus: () => undefined,
onBlur: () => undefined,
'data-testid': undefined,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/MaskedInput.tsx
Expand Up @@ -234,7 +234,7 @@ const MaskedInput = React.forwardRef<HTMLInputElement, MaskedInputProps>(
checkValue: string,
cursorPosition = checkValue.length
): boolean {
const patternRegex = new RegExp(pattern);
const patternRegex = new RegExp(`^${pattern}$`);
vbabich marked this conversation as resolved.
Show resolved Hide resolved
if (patternRegex.test(checkValue)) {
return true;
}
Expand Down
20 changes: 9 additions & 11 deletions packages/components/src/TimeInput.test.tsx
Expand Up @@ -150,6 +150,12 @@ describe('select and type', () => {
testSelectAndType(4, '55', '12:55:56');

testSelectAndType(1, '000000', '00:00:00');

// Jumps to the next section if the previous section is complete
testSelectAndType(0, '35', `03:54:56`);

// Validates the whole value, not just a substring
testSelectAndType(9, '11`"();', `12:34:11`);
});
it('handles backspace', () => {
// Replace selected section with fixed-width spaces
Expand Down Expand Up @@ -194,20 +200,12 @@ describe('select and type', () => {
unmount();
});

it('existing invalid behaviors that might need to be fixed', () => {
// Expected: '20:34:56'?
it('existing edge cases', () => {
// Ideally it should change the first section to 20, i.e. '20:34:56'
testSelectAndType(1, '5{arrowleft}2', `25:34:56`);
vbabich marked this conversation as resolved.
Show resolved Hide resolved

// Fill in with zeros when skipping positions. Expected: '03:34:56'
// Ideally it should fill in with zeros when skipping positions, i.e. '03:34:56'
testSelectAndType(0, '{backspace}3', `${FIXED_WIDTH_SPACE}3:34:56`);

// Not sure it's ok to skip to the next section when the input isn't valid for the current section
// Expected: '03:34:56'?
testSelectAndType(0, '35', `03:54:56`);

// Should validate whole value
// Expected: '12:34:11'
testSelectAndType(9, '11`"();', `12:34:11\`"();`);
});
});

Expand Down