import React, { useMemo, useState } from "react"; import type { FieldError, UseControllerProps } from "react-hook-form"; import type { XOR } from "ts-xor"; import DateTimePicker from "react-native-modal-datetime-picker"; import { Keyboard, View } from "react-native"; import type { Clearable } from "@jobber/hooks"; import { styles } from "./InputTime.style"; import { getTimeZoneOffsetInMinutes, roundUpToNearestMinutes } from "./utils"; import { useAtlantisContext } from "../AtlantisContext"; import { InputPressable } from "../InputPressable"; import { FormField } from "../FormField"; import type { InputFieldWrapperProps } from "../InputFieldWrapper"; import { useAtlantisI18n } from "../hooks/useAtlantisI18n"; import type { InputPressableProps } from "../InputPressable/InputPressable"; interface InputTimeBaseProps extends Pick, Pick { /** * Defaulted to "always" so user can clear the time whenever there's a value. */ readonly clearable?: Extract; /** * Add a custom value to display when no time is selected * @default undefined */ readonly emptyValueLabel?: string; /** * Adjusts the UX of the time picker based on where you'd use it. * * - `"granular"` - allows the user to pick a very specific time * - `"scheduling"` - only allows user to select between 5 minutes interval. * If your design is catered towards "scheduling", you should use this type. * * @default scheduling */ readonly type?: "granular" | "scheduling"; /** * Hide or show the timer icon. */ readonly showIcon?: boolean; } export interface InputTimeFormControlled extends InputTimeBaseProps { /** * Adding a `name` would make this component "Form controlled" and must be * nested within a `
` component. * * Cannot be declared if `value` prop is used. */ readonly name: string; /** * Shows an error message below the field and highlights it red when the * value is invalid. Only applies when nested within a `` component. */ readonly validations?: UseControllerProps["rules"]; /** * The callback that fires whenever a time gets selected. */ readonly onChange?: (value?: Date | null) => void; } interface InputTimeDevControlled extends InputTimeBaseProps { /** * The value shown on the field. This gets automatically formatted to the * account's time format. */ readonly value: Date | string | undefined; /** * The callback that fires whenever a time gets selected. */ readonly onChange: (value?: Date) => void; } export type InputTimeProps = XOR< InputTimeFormControlled, InputTimeDevControlled >; const LOCALE_24_HOURS = "en_GB"; const LOCALE_12_HOURS = "en_US"; function formatInvalidState( error: FieldError | undefined, invalid: InputFieldWrapperProps["invalid"], ): boolean | string { if (invalid) return invalid; if (error && error.message) { return error.message; } return Boolean(error); } export function InputTime(props: InputTimeProps) { if (props.name) { return ( {(field, error) => ( { field.onChange(newValue); field.onBlur(); props.onChange?.(newValue); }} invalid={formatInvalidState(error, props.invalid)} /> )} ); } return ; } function InternalInputTime({ clearable = "always", disabled, emptyValueLabel, invalid, placeholder, value, name, type = "scheduling", showMiniLabel = true, onChange, showIcon = true, }: InputTimeProps) { const [showPicker, setShowPicker] = useState(false); const { t, formatTime } = useAtlantisI18n(); const { timeZone, timeFormat } = useAtlantisContext(); const is24Hour = timeFormat === "HH:mm"; const dateTime = useMemo( () => (typeof value === "string" ? new Date(value) : value), [value], ); const formattedTime = useMemo(() => { if (dateTime) { return formatTime(dateTime); } return emptyValueLabel; }, [dateTime, emptyValueLabel]); const canClearTime = formattedTime === emptyValueLabel ? "never" : clearable; return ( ); function showDatePicker() { Keyboard.dismiss(); setShowPicker(true); } function handleConfirm(newValue: Date) { setShowPicker(false); onChange?.(newValue); } function handleCancel() { setShowPicker(false); // Call onChange with the current value to trigger form's validation. onChange?.(dateTime); } function handleClear() { // Returns null only for Form controlled scenarios due to a limitation of // react-hook-form that doesn't allow passing undefined to form values. if (name) { onChange?.(null); } else { onChange?.(undefined); } } } function getInitialPickerDate(date?: Date | null): Date { if (date) return date; return roundUpToNearestMinutes(new Date(), 30); } function getMinuteInterval(type: InputTimeBaseProps["type"]) { if (type === "granular") return 1; return 5; }