Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 9 additions & 6 deletions src/components/TimePicker/TimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,19 @@ function TimePicker(
const value = DateUtils.extractTime12Hour(defaultValue, showFullFormat);
const canUseTouchScreen = canUseTouchScreenDeviceCapabilities();

const [isError, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [selectionHour, setSelectionHour] = useState({start: 0, end: 0});
const [selectionMinute, setSelectionMinute] = useState(showFullFormat ? {start: 0, end: 0} : {start: 2, end: 2}); // we focus it by default so need to have selection on the end
const [selectionSecond, setSelectionSecond] = useState({start: 0, end: 0});
const [selectionMillisecond, setSelectionMillisecond] = useState(showFullFormat ? {start: 6, end: 6} : {start: 0, end: 0});
const [hours, setHours] = useState(() => DateUtils.get12HourTimeObjectFromDate(value, showFullFormat).hour);
const [minutes, setMinutes] = useState(() => DateUtils.get12HourTimeObjectFromDate(value, showFullFormat).minute);
const [seconds, setSeconds] = useState(() => DateUtils.get12HourTimeObjectFromDate(value, showFullFormat).seconds);
const [milliseconds, setMilliseconds] = useState(() => DateUtils.get12HourTimeObjectFromDate(value, showFullFormat).milliseconds);
const [amPmValue, setAmPmValue] = useState(() => DateUtils.get12HourTimeObjectFromDate(value, showFullFormat).period);

const [isError, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [selectionHour, setSelectionHour] = useState({start: 0, end: 0});
const [selectionMinute, setSelectionMinute] = useState(showFullFormat ? {start: 0, end: 0} : {start: minutes.length, end: minutes.length}); // we focus it by default so need to have selection on the end
const [selectionSecond, setSelectionSecond] = useState({start: 0, end: 0});
const [selectionMillisecond, setSelectionMillisecond] = useState(showFullFormat ? {start: milliseconds.length, end: milliseconds.length} : {start: 0, end: 0});

const lastPressedKey = useRef('');
const hourInputRef = useRef<TextInput | null>(null);
const minuteInputRef = useRef<TextInput | null>(null);
Expand Down Expand Up @@ -922,3 +923,5 @@ function TimePicker(
TimePicker.displayName = 'TimePicker';

export default React.forwardRef(TimePicker);

export type {TimePickerProps, TimePickerRef, TimePickerRefName};
135 changes: 135 additions & 0 deletions tests/ui/components/TimePickerTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {NavigationContainer} from '@react-navigation/native';
import {fireEvent, render, screen} from '@testing-library/react-native';
import React, {act, createRef} from 'react';
import type {TextInput, TextInputProps} from 'react-native';
import TimePicker from '@src/components/TimePicker/TimePicker';
import type {TimePickerProps, TimePickerRef} from '@src/components/TimePicker/TimePicker';

// Tests currently run against index.ios.ts source, where functions that call
// native code (such as `isFocused` or `setNativeProps`) are not implemented.
// We need to implement them manually since tests are not run on actual devices.
jest.mock('react-native/Libraries/Components/TextInput/TextInput', () => {
const originalReact: typeof React = jest.requireActual('react');

const TextInputMock = originalReact.forwardRef<TextInput, TextInputProps>((props, ref) => {
const [isFocused, setIsFocused] = originalReact.useState(false);

originalReact.useImperativeHandle(
ref,
() =>
({
focus: () => {
setIsFocused(true);
},
blur: () => {
setIsFocused(false);
},
isFocused: () => isFocused,
setNativeProps: () => {},
props,
}) as unknown as TextInput,
[isFocused, props],
);

return null;
});

return {
// eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true,
default: TextInputMock,
};
});

describe('TimePicker Component', () => {
const renderTimePicker = (props: Partial<TimePickerProps> = {}, ref?: React.Ref<TimePickerRef>) =>
render(
<NavigationContainer>
<TimePicker
ref={ref}
onSubmit={() => {}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</NavigationContainer>,
);

afterEach(() => {
jest.clearAllMocks();
});

describe('replaces digits with 0 when the backspace button is pressed', () => {
function pressBackspaceAndExpect(
ref: React.RefObject<TimePickerRef | null>,
expected: {
hours: string;
minutes: string;
seconds?: string;
milliseconds?: string;
},
) {
const backspaceBtn = screen.getByTestId('button_<');
fireEvent.press(backspaceBtn);

expect(ref.current?.hourRef?.props.value).toBe(expected.hours);
expect(ref.current?.minuteRef?.props.value).toBe(expected.minutes);
expect(ref.current?.secondRef?.props.value).toBe(expected.seconds);
expect(ref.current?.millisecondRef?.props.value).toBe(expected.milliseconds);
}

it('when showFullFormat=true', () => {
const ref = createRef<TimePickerRef>();
renderTimePicker({defaultValue: '2025-01-01 12:34:56.789 AM', showFullFormat: true}, ref);

act(() => {
ref.current?.millisecondRef?.focus();
});

const backspaceBtn = screen.getByTestId('button_<');

// Milliseconds
pressBackspaceAndExpect(ref, {hours: '12', minutes: '34', seconds: '56', milliseconds: '780'});
pressBackspaceAndExpect(ref, {hours: '12', minutes: '34', seconds: '56', milliseconds: '700'});
pressBackspaceAndExpect(ref, {hours: '12', minutes: '34', seconds: '56', milliseconds: '000'});

fireEvent.press(backspaceBtn); // Skip separator

// Seconds
pressBackspaceAndExpect(ref, {hours: '12', minutes: '34', seconds: '50', milliseconds: '000'});
pressBackspaceAndExpect(ref, {hours: '12', minutes: '34', seconds: '00', milliseconds: '000'});

fireEvent.press(backspaceBtn); // Skip separator

// Minutes
pressBackspaceAndExpect(ref, {hours: '12', minutes: '30', seconds: '00', milliseconds: '000'});
pressBackspaceAndExpect(ref, {hours: '12', minutes: '00', seconds: '00', milliseconds: '000'});

fireEvent.press(backspaceBtn); // Skip separator

// Hours
pressBackspaceAndExpect(ref, {hours: '10', minutes: '00', seconds: '00', milliseconds: '000'});
pressBackspaceAndExpect(ref, {hours: '00', minutes: '00', seconds: '00', milliseconds: '000'});
});

it('when showFullFormat=false', () => {
const ref = createRef<TimePickerRef>();
renderTimePicker({defaultValue: '2025-01-01 12:34 AM', showFullFormat: false}, ref);

act(() => {
ref.current?.minuteRef?.focus();
});

const backspaceBtn = screen.getByTestId('button_<');

// Minutes
pressBackspaceAndExpect(ref, {hours: '12', minutes: '30'});
pressBackspaceAndExpect(ref, {hours: '12', minutes: '00'});

fireEvent.press(backspaceBtn); // Skip separator

// Hours
pressBackspaceAndExpect(ref, {hours: '10', minutes: '00'});
pressBackspaceAndExpect(ref, {hours: '00', minutes: '00'});
});
});
});
Loading