Skip to content

Conversation

@dayleclarke
Copy link

@dayleclarke dayleclarke commented Sep 18, 2025

Time wheel: enable visual looping (…23→00 / 59→00…).

Description

This PR modifies wheel-picker.tsx so the hour/minute wheels loop visually (e.g., 23→00, 59→00) instead of stopping at the ends. Previously on Android and iOS the list ended at the last option (e.g., 23 or 59). To mimic the web experience and common native pickers, this change allows both wheels to “wrap” so users can keep scrolling in either direction.

Looping was implemented by physically repeating the option list inside the FlatList and mapping the scrolled row back to the base option via a small helper. Selection math, snapping, and UI style remain unchanged.

Changes

  1. Repeat the data to create the loop. Physical repetition gives real rows after the end, so scrolling past 23 lands on the next block’s 00 without any code-driven recenter.
const REPEAT = 7;                        // odd ⇒ has a true middle block
const MID_BLOCK = Math.floor(REPEAT / 2);
const wrap = (i: number, len: number) =>
  (len ? ((i % len) + len) % len : 0);   // Wrap index into [0,len-1]
const paddedOptions = useMemo(() => {
  const core: (PickerOption | null)[] = [];
  for (let b = 0; b < REPEAT; b++) core.push(...options); // A A A A A A A
  for (let i = 0; i < visibleRest; i++) { core.unshift(null); core.push(null); } // keep existing padding
  return core;
}, [options, visibleRest]);
  1. Start in the middle block to avoid hitting the physical ends immediately.(preserves scroll room both ways). We seed via offset because indices now span the repeated list.
const baseLen = options.length;
const baseSelectedIndex = Math.max(0, options.findIndex(it => it.value === value));
const initialTopIndex = MID_BLOCK * baseLen + baseSelectedIndex;
const initialOffset = initialTopIndex * itemHeight;

useEffect(() => {
  // seed position by offset to the middle block
  flatListRef.current?.scrollToOffset({ offset: initialOffset, animated: false });
}, [initialOffset]);
  1. Map scroll position back to base option (no recentering). The original “top row determines value” approach was kept and it simply wraps into [0..len-1]. No auto-recenter, same snap feel.
const handleScrollEnd = (e) => {
  const y = Math.max(0, e.nativeEvent.contentOffset.y);
  let topIdx = Math.floor(y / itemHeight);
  if (y % itemHeight > itemHeight / 2) topIdx++;
  const baseIdx = wrap(topIdx, baseLen);     //  …22,23,24→0,25→1,…
  if (baseIdx !== baseSelectedIndex) onChange(options[baseIdx].value);
};
image

Bug Fix: Unexpected Initial Time Change on Time Picker load

Unexpectedly this also fixed a bug/issue where the time was reseting to 00:00 when you clicked on the header (I had a seperate branch where I fixed this issue but adding this feature seemed to have also resolved that bug making my previous branch redundant, so I didn't push these changes or make a PR for it. )

Before this change, the wheel could emit an unintended 0 during mount or view switches: using initialScrollIndex on a padded list sometimes caused an early “settle” at the top, the index could be out-of-range/undefined and then clamped or coerced (e.g. || 0), and the wheel would call onChange(0)—overwriting the parent’s seeded time and making it look like it snapped to 00:00.
This PR hardens the wheel so that can’t happen: we seed via scrollToOffset into the middle block (no initialScrollIndex), ignore the first settle (no mount-time emit), wrap indices instead of clamping, and emit the exact option value (no || 0). As a result, the wheel no longer pushes 00:00 back into state, and the header-seeded time persists.
Fixes: #169

@dayleclarke
Copy link
Author

As I said in the description for this PR I hadn't expected my feature to resolved the bug where the time was unexpectedly changing when you open the time picker from the time in the header in the date picker. By seeding via scrollToOffset (no initialScrollIndex), ignoring the first settle, wrapping indices (not clamping), and emitting the exact value (no || 0), the wheel no longer emits a stray 0 on mount/view switch—so the header-seeded time isn’t overwritten.
I fixed this issue in a totally different way with changes to the datetime-picker.tsx and time-picker.tsx. Please let me know if you would prefer a parent-side fix instead I made a PR with this alternative method of fixing the issue.

@bun-beos
Copy link

bun-beos commented Oct 6, 2025

How can i use your change for my local source? i tried to edit the source file of this library but it doesn't work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TimePicker] Pressing the time selector triggers onChange

2 participants