diff --git a/README.md b/README.md index e243a0d..b21acb4 100644 --- a/README.md +++ b/README.md @@ -446,26 +446,33 @@ return ( | Prop | Description | Type | Default | Required | | :------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------: | :------: | -| onDurationChange | Callback when the duration changes | `(duration: { hours: number, minutes: number, seconds: number }) => void` | - | false | -| initialValue | Initial value for the picker | `{ hours?: number, minutes?: number, seconds?: number }` | - | false | +| onDurationChange | Callback when the duration changes | `(duration: { days: number, hours: number, minutes: number, seconds: number }) => void` | - | false | +| initialValue | Initial value for the picker | `{ days?: number, hours?: number, minutes?: number, seconds?: number }` | - | false | +| hideDays | Hide the days picker | Boolean | true | false | | hideHours | Hide the hours picker | Boolean | false | false | | hideMinutes | Hide the minutes picker | Boolean | false | false | | hideSeconds | Hide the seconds picker | Boolean | false | false | -| hoursPickerIsDisabled | Disable the hours picker picker | Boolean | false | false | -| minutesPickerIsDisabled | Disable the minutes picker picker | Boolean | false | false | -| secondsPickerIsDisabled | Disable the seconds picker picker | Boolean | false | false | +| daysPickerIsDisabled | Disable the days picker | Boolean | false | false | +| hoursPickerIsDisabled | Disable the hours picker | Boolean | false | false | +| minutesPickerIsDisabled | Disable the minutes picker | Boolean | false | false | +| secondsPickerIsDisabled | Disable the seconds picker | Boolean | false | false | +| dayLimit | Limit on the days it is possible to select | `{ max?: Number, min?: Number }` | - | false | | hourLimit | Limit on the hours it is possible to select | `{ max?: Number, min?: Number }` | - | false | | minuteLimit | Limit on the minutes it is possible to select | `{ max?: Number, min?: Number }` | - | false | | secondLimit | Limit on the seconds it is possible to select | `{ max?: Number, min?: Number }` | - | false | +| maximumDays | The highest value on the days picker | Number | 23 | false | | maximumHours | The highest value on the hours picker | Number | 23 | false | | maximumMinutes | The highest value on the minutes picker | Number | 59 | false | | maximumSeconds | The highest value on the seconds picker | Number | 59 | false | +| dayInterval | The interval between values on the days picker | Number | 1 | false | | hourInterval | The interval between values on the hours picker | Number | 1 | false | | minuteInterval | The interval between values on the minutes picker | Number | 1 | false | | secondInterval | The interval between values on the seconds picker | Number | 1 | false | +| dayLabel | Label for the days picker | String \| React.ReactElement | d | false | | hourLabel | Label for the hours picker | String \| React.ReactElement | h | false | | minuteLabel | Label for the minutes picker | String \| React.ReactElement | m | false | | secondLabel | Label for the seconds picker | String \| React.ReactElement | s | false | +| padDaysWithZero | Pad single-digit days in the picker with a zero | Boolean | false | false | | padHoursWithZero | Pad single-digit hours in the picker with a zero | Boolean | false | false | | padMinutesWithZero | Pad single-digit minutes in the picker with a zero | Boolean | true | false | | padSecondsWithZero | Pad single-digit seconds in the picker with a zero | Boolean | true | false | @@ -475,6 +482,7 @@ return ( | use12HourPicker | Switch the hour picker to 12-hour format with an AM / PM label | Boolean | false | false | | amLabel | Set the AM label if using the 12-hour picker | String | am | false | | pmLabel | Set the PM label if using the 12-hour picker | String | pm | false | +| repeatDayNumbersNTimes | Set the number of times the list of days is repeated in the picker | Number | 3 | false | | repeatHourNumbersNTimes | Set the number of times the list of hours is repeated in the picker | Number | 7 | false | | repeatMinuteNumbersNTimes | Set the number of times the list of minutes is repeated in the picker | Number | 3 | false | | repeatSecondNumbersNTimes | Set the number of times the list of seconds is repeated in the picker | Number | 3 | false | @@ -484,7 +492,7 @@ return ( | Haptics | [Haptics Namespace (required for Haptic feedback)](#haptic-feedback) | [expo-haptics](https://www.npmjs.com/package/expo-haptics) | - | false | | Audio | [Audio Class (required for audio feedback i.e. click sound)](#audio-feedback-click-sound) | [expo-av](https://www.npmjs.com/package/expo-av).Audio | - | false | | pickerFeedback | [Generic picker feedback as alternative to the above Expo feedback support](#generic-feedback) | `() => void \| Promise ` | - | false | -| FlatList | FlatList component used internally to implement each picker (hour, minutes and seconds). More info [below](#custom-flatlist) | [react-native](https://reactnative.dev/docs/flatlist).FlatList | `FlatList` from `react-native` | false | +| FlatList | FlatList component used internally to implement each picker (day, hour, minutes and seconds). More info [below](#custom-flatlist) | [react-native](https://reactnative.dev/docs/flatlist).FlatList | `FlatList` from `react-native` | false | | clickSoundAsset | Custom sound asset for click sound (required for offline click sound - download default [here](https://drive.google.com/uc?export=download&id=10e1YkbNsRh-vGx1jmS1Nntz8xzkBp4_I)) | require(.../somefolderpath) or {uri: www.someurl} | - | false | | pickerContainerProps | Props for the picker container | `React.ComponentProps` | - | false | | pickerGradientOverlayProps | Props for the gradient overlay (supply a different `locations` array to adjust its position) overlays | `Partial` | - | false | @@ -519,7 +527,7 @@ Note the minor limitations to the allowed styles for `pickerContainer` and `pick When the `disableInfiniteScroll` prop is not set, the picker gives the appearance of an infinitely scrolling picker by auto-scrolling forward/back when you near the start/end of the list. When the picker auto-scrolls, a momentary flicker is visible if you are scrolling very slowly. -To mitigate for this, the list of numbers in each picker is repeated a given number of times based on the length of the list (8 times for the hours picker, and 3 times for the minutes and seconds picker). These have a performance trade-off: higher values mean the picker has to auto-scroll less to maintain the infinite scroll, but has to render a longer list of numbers. The number of repetitions automatically adjusts if the number of items in the picker changes (e.g. if an interval is included, or the maximum value is modified), balancing the trade-off. You can also manually adjust the number of repetitions in each picker with the `repeatHourNumbersNTimes`, `repeatMinuteNumbersNTimes` and `repeatSecondNumbersNTimes` props. +To mitigate for this, the list of numbers in each picker is repeated a given number of times based on the length of the list (7 times for the hours picker, and 3 times for the days/minutes/seconds picker). These have a performance trade-off: higher values mean the picker has to auto-scroll less to maintain the infinite scroll, but has to render a longer list of numbers. The number of repetitions automatically adjusts if the number of items in the picker changes (e.g. if an interval is included, or the maximum value is modified), balancing the trade-off. You can also manually adjust the number of repetitions in each picker with the `repeatHourNumbersNTimes`, `repeatMinuteNumbersNTimes` and `repeatSecondNumbersNTimes` props. Note that you can avoid the auto-scroll flickering entirely by disabling infinite scroll. You could then set the above props to high values, so that a user has to scroll far down/up the list to reach the end of the list. diff --git a/src/components/TimerPicker/index.tsx b/src/components/TimerPicker/index.tsx index 4e8ea92..82e27fb 100644 --- a/src/components/TimerPicker/index.tsx +++ b/src/components/TimerPicker/index.tsx @@ -22,7 +22,12 @@ const TimerPicker = forwardRef( aggressivelyGetLatestDuration = false, allowFontScaling = false, amLabel = "am", + dayInterval = 1, + dayLabel, + dayLimit, + daysPickerIsDisabled = false, disableInfiniteScroll = false, + hideDays = true, hideHours = false, hideMinutes = false, hideSeconds = false, @@ -31,6 +36,7 @@ const TimerPicker = forwardRef( hourLimit, hoursPickerIsDisabled = false, initialValue, + maximumDays = 30, maximumHours = 23, maximumMinutes = 59, maximumSeconds = 59, @@ -39,12 +45,14 @@ const TimerPicker = forwardRef( minuteLimit, minutesPickerIsDisabled = false, onDurationChange, + padDaysWithZero = false, padHoursWithZero = false, padMinutesWithZero = true, padSecondsWithZero = true, padWithNItems = 1, pickerContainerProps, pmLabel = "pm", + repeatDayNumbersNTimes = 3, repeatHourNumbersNTimes = 8, repeatMinuteNumbersNTimes = 3, repeatSecondNumbersNTimes = 3, @@ -74,11 +82,17 @@ const TimerPicker = forwardRef( const safeInitialValue = useMemo( () => getSafeInitialValue({ + days: initialValue?.days, hours: initialValue?.hours, minutes: initialValue?.minutes, seconds: initialValue?.seconds, }), - [initialValue?.hours, initialValue?.minutes, initialValue?.seconds] + [ + initialValue?.days, + initialValue?.hours, + initialValue?.minutes, + initialValue?.seconds, + ] ); const styles = useMemo( @@ -87,6 +101,7 @@ const TimerPicker = forwardRef( [customStyles] ); + const [selectedDays, setSelectedDays] = useState(safeInitialValue.days); const [selectedHours, setSelectedHours] = useState( safeInitialValue.hours ); @@ -99,30 +114,36 @@ const TimerPicker = forwardRef( useEffect(() => { onDurationChange?.({ + days: selectedDays, hours: selectedHours, minutes: selectedMinutes, seconds: selectedSeconds, }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedHours, selectedMinutes, selectedSeconds]); + }, [selectedDays, selectedHours, selectedMinutes, selectedSeconds]); + const daysDurationScrollRef = useRef(null); const hoursDurationScrollRef = useRef(null); const minutesDurationScrollRef = useRef(null); const secondsDurationScrollRef = useRef(null); useImperativeHandle(ref, () => ({ reset: (options) => { + setSelectedDays(safeInitialValue.days); setSelectedHours(safeInitialValue.hours); setSelectedMinutes(safeInitialValue.minutes); setSelectedSeconds(safeInitialValue.seconds); + daysDurationScrollRef.current?.reset(options); hoursDurationScrollRef.current?.reset(options); minutesDurationScrollRef.current?.reset(options); secondsDurationScrollRef.current?.reset(options); }, setValue: (value, options) => { + setSelectedDays(value.days); setSelectedHours(value.hours); setSelectedMinutes(value.minutes); setSelectedSeconds(value.seconds); + daysDurationScrollRef.current?.setValue(value.days, options); hoursDurationScrollRef.current?.setValue(value.hours, options); minutesDurationScrollRef.current?.setValue( value.minutes, @@ -134,6 +155,7 @@ const TimerPicker = forwardRef( ); }, latestDuration: { + days: daysDurationScrollRef.current?.latestDuration, hours: hoursDurationScrollRef.current?.latestDuration, minutes: minutesDurationScrollRef.current?.latestDuration, seconds: secondsDurationScrollRef.current?.latestDuration, @@ -145,6 +167,32 @@ const TimerPicker = forwardRef( {...pickerContainerProps} style={styles.pickerContainer} testID="timer-picker"> + {!hideDays ? ( + + ) : null} {!hideHours ? ( StyleSheet.create({ pickerContainer: { diff --git a/src/components/TimerPicker/types.ts b/src/components/TimerPicker/types.ts index ca73f46..2a6fc63 100644 --- a/src/components/TimerPicker/types.ts +++ b/src/components/TimerPicker/types.ts @@ -13,6 +13,7 @@ import type { CustomTimerPickerStyles } from "./styles"; export interface TimerPickerRef { latestDuration: { + days: MutableRefObject | undefined; hours: MutableRefObject | undefined; minutes: MutableRefObject | undefined; seconds: MutableRefObject | undefined; @@ -20,6 +21,7 @@ export interface TimerPickerRef { reset: (options?: { animated?: boolean }) => void; setValue: ( value: { + days: number; hours: number; minutes: number; seconds: number; @@ -42,7 +44,12 @@ export interface TimerPickerProps { allowFontScaling?: boolean; amLabel?: string; clickSoundAsset?: SoundAssetType; + dayInterval?: number; + dayLabel?: string | React.ReactElement; + dayLimit?: LimitType; + daysPickerIsDisabled?: boolean; disableInfiniteScroll?: boolean; + hideDays?: boolean; hideHours?: boolean; hideMinutes?: boolean; hideSeconds?: boolean; @@ -51,10 +58,12 @@ export interface TimerPickerProps { hourLimit?: LimitType; hoursPickerIsDisabled?: boolean; initialValue?: { + days?: number; hours?: number; minutes?: number; seconds?: number; }; + maximumDays?: number; maximumHours?: number; maximumMinutes?: number; maximumSeconds?: number; @@ -63,10 +72,12 @@ export interface TimerPickerProps { minuteLimit?: LimitType; minutesPickerIsDisabled?: boolean; onDurationChange?: (duration: { + days: number; hours: number; minutes: number; seconds: number; }) => void; + padDaysWithZero?: boolean; padHoursWithZero?: boolean; padMinutesWithZero?: boolean; padSecondsWithZero?: boolean; @@ -75,6 +86,7 @@ export interface TimerPickerProps { pickerFeedback?: () => void | Promise; pickerGradientOverlayProps?: Partial; pmLabel?: string; + repeatDayNumbersNTimes?: number; repeatHourNumbersNTimes?: number; repeatMinuteNumbersNTimes?: number; repeatSecondNumbersNTimes?: number; diff --git a/src/components/TimerPickerModal/index.tsx b/src/components/TimerPickerModal/index.tsx index 917ecb6..6cd17fd 100644 --- a/src/components/TimerPickerModal/index.tsx +++ b/src/components/TimerPickerModal/index.tsx @@ -48,6 +48,7 @@ const TimerPickerModal = forwardRef( const timerPickerRef = useRef(null); const safeInitialValue = getSafeInitialValue({ + days: initialValue?.days, hours: initialValue?.hours, minutes: initialValue?.minutes, seconds: initialValue?.seconds, @@ -69,6 +70,7 @@ const TimerPickerModal = forwardRef( reset(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ + safeInitialValue.days, safeInitialValue.hours, safeInitialValue.minutes, safeInitialValue.seconds, @@ -76,6 +78,7 @@ const TimerPickerModal = forwardRef( const hideModalHandler = () => { setSelectedDuration({ + days: confirmedDuration.days, hours: confirmedDuration.hours, minutes: confirmedDuration.minutes, seconds: confirmedDuration.seconds, @@ -87,6 +90,7 @@ const TimerPickerModal = forwardRef( const latestDuration = timerPickerRef.current?.latestDuration; const newDuration = { + days: latestDuration?.days?.current ?? selectedDuration.days, hours: latestDuration?.hours?.current ?? selectedDuration.hours, minutes: latestDuration?.minutes?.current ?? @@ -107,7 +111,12 @@ const TimerPickerModal = forwardRef( // wrapped in useCallback to avoid unnecessary re-renders of TimerPicker const durationChangeHandler = useCallback( - (duration: { hours: number; minutes: number; seconds: number }) => { + (duration: { + days: number; + hours: number; + minutes: number; + seconds: number; + }) => { setSelectedDuration(duration); onDurationChange?.(duration); }, @@ -122,6 +131,7 @@ const TimerPickerModal = forwardRef( timerPickerRef.current?.setValue(value, options); }, latestDuration: { + days: timerPickerRef.current?.latestDuration?.days, hours: timerPickerRef.current?.latestDuration?.hours, minutes: timerPickerRef.current?.latestDuration?.minutes, seconds: timerPickerRef.current?.latestDuration?.seconds, diff --git a/src/components/TimerPickerModal/types.ts b/src/components/TimerPickerModal/types.ts index f593daf..57a3ff1 100644 --- a/src/components/TimerPickerModal/types.ts +++ b/src/components/TimerPickerModal/types.ts @@ -9,6 +9,7 @@ import type { CustomTimerPickerModalStyles } from "./styles"; export interface TimerPickerModalRef { latestDuration: { + days: MutableRefObject | undefined; hours: MutableRefObject | undefined; minutes: MutableRefObject | undefined; seconds: MutableRefObject | undefined; @@ -16,6 +17,7 @@ export interface TimerPickerModalRef { reset: (options?: { animated?: boolean }) => void; setValue: ( value: { + days: number; hours: number; minutes: number; seconds: number; @@ -38,10 +40,12 @@ export interface TimerPickerModalProps extends TimerPickerProps { modalTitleProps?: React.ComponentProps; onCancel?: () => void; onConfirm: ({ + days, hours, minutes, seconds, }: { + days: number; hours: number; minutes: number; seconds: number; diff --git a/src/tests/TimerPicker.test.tsx b/src/tests/TimerPicker.test.tsx index 17af3ec..409c9a1 100644 --- a/src/tests/TimerPicker.test.tsx +++ b/src/tests/TimerPicker.test.tsx @@ -18,12 +18,14 @@ describe("TimerPicker", () => { expect(component).toBeDefined(); }); - it("hides minutes and seconds when respective hide props are provided", () => { + it("hides days, minutes and seconds when respective hide props are provided", () => { const { queryByTestId } = render( - + ); + const dayPicker = queryByTestId("duration-scroll-day"); const minutePicker = queryByTestId("duration-scroll-minute"); const secondPicker = queryByTestId("duration-scroll-second"); + expect(dayPicker).toBeNull(); expect(minutePicker).toBeNull(); expect(secondPicker).toBeNull(); }); @@ -36,6 +38,6 @@ describe("TimerPicker", () => { ); const customFlatList = queryAllByTestId("custom-flat-list"); - expect(customFlatList).toHaveLength(3); + expect(customFlatList).toHaveLength(4); }); }); diff --git a/src/utils/getSafeInitialValue.ts b/src/utils/getSafeInitialValue.ts index 5f0aef2..46c46fa 100644 --- a/src/utils/getSafeInitialValue.ts +++ b/src/utils/getSafeInitialValue.ts @@ -1,12 +1,17 @@ export const getSafeInitialValue = ( initialValue: | { + days?: number; hours?: number; minutes?: number; seconds?: number; } | undefined ) => ({ + days: + typeof initialValue?.days === "number" && !isNaN(initialValue?.days) + ? initialValue.days + : 0, hours: typeof initialValue?.hours === "number" && !isNaN(initialValue?.hours) ? initialValue.hours