Skip to content

Commit 102fee1

Browse files
committed
fix(DateInput): clearing on touch no longer scrolls the page
The touch surface's clear (X) hands focus back to the field with a bare focus(). iOS Safari scrolls the document to bring a newly focused input into view, so the page slides under the user's finger mid-tap — even though the field they just tapped is by definition already on screen. Focus is now handed back with preventScroll: true, the same guard the rest of this surface already uses (MonthScroller's day focus, BottomSheet's present/dismiss focus). Measured on the iOS 26 simulator with a real touch sequence, mid-page field: +12px before, 0 after; focus still lands on the field either way.
1 parent b594d5c commit 102fee1

3 files changed

Lines changed: 46 additions & 1 deletion

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@astryxdesign/core': patch
3+
---
4+
5+
[fix] DateInput: clearing on touch no longer scrolls the page
6+
7+
The touch surface's clear (✕) returned focus to the field with a bare
8+
`focus()`. iOS Safari scrolls the document to bring a newly focused input into
9+
view, so the page slid under the user's finger mid-tap — even though the field
10+
they had just tapped was by definition already on screen. Focus is now handed
11+
back with `preventScroll: true`, the same guard MonthScroller and BottomSheet
12+
already use. Measured on the iOS 26 simulator: +12px before, 0 after.
13+
14+
@imdreamrunner

packages/core/src/DateInput/DateInputTouch.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,30 @@ describe('DateInput — field parity', () => {
419419
expect(onChange).toHaveBeenCalledWith(undefined);
420420
});
421421

422+
it('returns focus to the field without letting the page scroll', () => {
423+
// iOS Safari scrolls the document to reveal a newly focused input. The
424+
// field the user just tapped is already on screen, so that scroll is pure
425+
// jank — the page slides under the finger mid-tap. jsdom implements no
426+
// scrolling, so the guard is asserted at the call: focus must be given
427+
// preventScroll. Measured on the iOS 26 simulator: +12px without it, 0
428+
// with it.
429+
render(
430+
<DateInput
431+
label="Ship date"
432+
value="2026-03-21"
433+
hasClear
434+
onChange={() => {}}
435+
/>,
436+
);
437+
const input = field();
438+
const focus = vi.spyOn(input, 'focus');
439+
440+
fireEvent.click(screen.getByRole('button', {name: /Clear Ship date/}));
441+
442+
expect(focus).toHaveBeenCalledWith({preventScroll: true});
443+
focus.mockRestore();
444+
});
445+
422446
it('does not open the picker until the field is tapped', () => {
423447
withLayout(() => {
424448
render(<DateInput label="Ship date" onChange={() => {}} />);

packages/core/src/DateInput/TouchDateField.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,14 @@ export function TouchDateField({
708708

709709
const handleClear = useCallback(() => {
710710
fireChange(undefined);
711-
inputRef.current?.focus();
711+
// preventScroll: the field the user just tapped is by definition already
712+
// on screen, so there is nothing to reveal — but iOS Safari scrolls the
713+
// document to bring a newly focused input into view regardless, and the
714+
// page slides under the finger mid-tap. Measured on the iOS 26 simulator:
715+
// +12px without it on a mid-page field, 0 with it. This is the same guard
716+
// the rest of the touch surface already uses (MonthScroller's day focus,
717+
// BottomSheet's present/dismiss focus).
718+
inputRef.current?.focus({preventScroll: true});
712719
}, [fireChange]);
713720

714721
/**

0 commit comments

Comments
 (0)