Skip to content

Commit

Permalink
InputTime takes formatTime for custom i18n time string formatting (
Browse files Browse the repository at this point in the history
…#237)

* InputTime takes formatTime function for custom i18n time string formatting

* Add no-op onChange to a test component to resolve proptypes warning

* Add test for InputTime's formatTime prop.
  • Loading branch information
pixelbandito authored Jul 23, 2020
1 parent 8f83b6c commit 845bbee
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 20 deletions.
32 changes: 22 additions & 10 deletions src/components/InputTime/AsString.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import styles from './InputTime.module.css';

export interface AsStringProps
extends Omit<InputProps, 'value' | 'max' | 'min'> {
formatTime?: (date: Date) => string;
fuzzyInputProps?: InputProps;
max?: string;
min?: string;
Expand All @@ -40,6 +41,7 @@ export interface AsStringProps
const AsString: React.FC<AsStringProps> = ({
className = '',
disabled,
formatTime,
forwardedRef,
fuzzyInputProps = {},
max,
Expand Down Expand Up @@ -101,7 +103,7 @@ const AsString: React.FC<AsStringProps> = ({

// Fuzzy value is user input.
const [fuzzyValue, setFuzzyValue] = useState(
value ? getLocaleTimeStringFromShortTimeString(value) : ''
value ? getLocaleTimeStringFromShortTimeString(value, { formatTime }) : ''
);

const syncValidity = (
Expand Down Expand Up @@ -142,16 +144,21 @@ const AsString: React.FC<AsStringProps> = ({
};

const handleBlurFuzzyValue = () => {
setFuzzyValue(value ? getLocaleTimeStringFromShortTimeString(value) : '');
setFuzzyValue(
value ? getLocaleTimeStringFromShortTimeString(value, { formatTime }) : ''
);
};

const handleSelectMenuItem = (e: { target: { value: Date } }) => {
const dateTime = e.target.value;

const label = dateTime.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});
const label =
typeof formatTime === 'function'
? formatTime(dateTime)
: dateTime.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});

const timeString = getShortTimeString(
dateTime.getHours(),
Expand All @@ -170,11 +177,15 @@ const AsString: React.FC<AsStringProps> = ({
}

if (localRef?.current && document.activeElement !== localRef?.current) {
setFuzzyValue(value ? getLocaleTimeStringFromShortTimeString(value) : '');
setFuzzyValue(
value
? getLocaleTimeStringFromShortTimeString(value, { formatTime })
: ''
);
}

syncValidity(shadowTimeInputRef, localRef);
}, [localRef, shadowTimeInputRef, step, value]);
}, [formatTime, localRef, shadowTimeInputRef, step, value]);

// Sync validity on min/max changes
useEffect(() => {
Expand Down Expand Up @@ -212,16 +223,17 @@ const AsString: React.FC<AsStringProps> = ({
/>
{showDropdown && (
<Dropdown
toggleAriaLabel={toggleAriaLabel}
className={styles.addons}
disabled={disabled}
formatTime={formatTime}
max={max}
min={min}
value={value}
onSelectMenuItem={handleSelectMenuItem}
showDropdown={showDropdown}
step={dropdownStep}
stepFrom={stepFrom}
toggleAriaLabel={toggleAriaLabel}
value={value}
/>
)}
</div>
Expand Down
24 changes: 19 additions & 5 deletions src/components/InputTime/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import styles from './Dropdown.module.css';
interface DropdownProps {
className?: string;
disabled?: boolean;
formatTime?: (date: Date) => string;
max?: string;
min?: string;
onSelectMenuItem: Function;
Expand All @@ -36,6 +37,7 @@ interface DropdownProps {
const Dropdown: React.FC<DropdownProps> = ({
className = '',
disabled,
formatTime,
max,
min,
onSelectMenuItem,
Expand Down Expand Up @@ -94,10 +96,13 @@ const Dropdown: React.FC<DropdownProps> = ({
do {
attempts += 1;

const label = current.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});
const label =
typeof formatTime === 'function'
? formatTime(current)
: current.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});

if (
(maxDate === undefined || current <= maxDate) &&
Expand All @@ -116,7 +121,16 @@ const Dropdown: React.FC<DropdownProps> = ({
} while (attempts <= maxAttempts && current < end);

return options;
}, [customStep, max, min, value, onSelectMenuItem, showDropdown, stepFrom]);
}, [
customStep,
formatTime,
max,
min,
onSelectMenuItem,
showDropdown,
stepFrom,
value,
]);

return (
<EasyDropdown
Expand Down
35 changes: 35 additions & 0 deletions src/components/InputTime/InputTime.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('InputTime', () => {
<input
data-testid="InputTime test"
min="01:30"
onChange={() => {}}
step="2700"
value="01:30"
/>
Expand Down Expand Up @@ -125,6 +126,40 @@ describe('InputTime', () => {
});
});

describe('when passed a custom `formatTime` prop', () => {
it('uses the custom formatter for the fuzzy input', () => {
const { getByLabelText } = render(
<InputTime
formatTime={(date: Date) =>
`${date.getHours()}🎈:${date.getMinutes()}🐝`
}
fuzzyInputProps={{ 'aria-label': 'time input' }}
value="01:30"
/>
);

const baseInputTime = getByLabelText('time input') as HTMLInputElement;
expect(baseInputTime.value).toEqual('1🎈:30🐝');
});

it('uses the custom formatter for the dropdown options', () => {
const { getByLabelText, queryByText } = render(
<InputTime
formatTime={(date: Date) =>
`${date.getHours()}🎈:${date.getMinutes()}🐝`
}
toggleAriaLabel="Toggle"
/>
);

const toggle = getByLabelText('Toggle');
// Open dropdown
userEvent.click(toggle);
const select10 = queryByText('10🎈:0🐝');
expect(select10).toBeTruthy();
});
});

describe('dropdown time picker', () => {
it('clicking a time selects it', () => {
const handleChange = jest.fn((e) => {
Expand Down
11 changes: 11 additions & 0 deletions src/components/InputTime/story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,17 @@ storiesOf('Planets/InputTime', module)
value={defaultValueDate}
/>
</Wrap>
<Wrap>
<Title>With a custom `formatTime` function</Title>
<InteractiveInput
Component={InputTime}
formatTime={(date: Date) =>
`${date.getHours()}🎈:${date.getMinutes()}🐝`
}
onChange={action('onChange date')}
value={new Date().toISOString()}
/>
</Wrap>
<Wrap>
<Title>Requires you to pick a future date time</Title>
<InteractiveInput
Expand Down
15 changes: 10 additions & 5 deletions src/components/InputTime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,16 @@ export const getDateTimeFromShortTimeString = (value: string) => {
return date;
};

export const getLocaleTimeStringFromShortTimeString = (value: string) =>
getDateTimeFromShortTimeString(value).toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});
export const getLocaleTimeStringFromShortTimeString = (
value: string,
{ formatTime }
) =>
typeof formatTime === 'function'
? formatTime(getDateTimeFromShortTimeString(value))
: getDateTimeFromShortTimeString(value).toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});

export const guessTimeFromString = (string: string) => {
const invalidChars = new RegExp('[^\\d:\\spam.]', 'gi');
Expand Down

0 comments on commit 845bbee

Please sign in to comment.