Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 8 additions & 52 deletions packages/components/src/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -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>(
}
);

DateInput.displayName = 'DateInput';

DateInput.defaultProps = {
className: '',
onChange: () => false,
Expand Down
48 changes: 48 additions & 0 deletions packages/components/src/DateInputUtils.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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}$`);
if (patternRegex.test(checkValue)) {
return true;
}
Expand Down
20 changes: 9 additions & 11 deletions packages/components/src/TimeInput.test.tsx
Original file line number Diff line number Diff line change
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`);

// 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