Skip to content

Commit 532bfaf

Browse files
authored
Prevent date picker scrolling the whole page when focused (#2606)
1 parent b826620 commit 532bfaf

File tree

5 files changed

+77
-59
lines changed

5 files changed

+77
-59
lines changed

packages/@react-aria/datepicker/src/useDateSegment.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {DatePickerFieldState, DateSegment} from '@react-stately/datepicker';
1414
import {DatePickerProps, DateValue} from '@react-types/datepicker';
1515
import {DOMProps} from '@react-types/shared';
16-
import {isIOS, isMac, mergeProps, useEvent, useId} from '@react-aria/utils';
16+
import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoView, useEvent, useId} from '@react-aria/utils';
1717
import {labelIds} from './useDateField';
1818
import {NumberParser} from '@internationalized/number';
1919
import React, {HTMLAttributes, RefObject, useMemo, useRef} from 'react';
@@ -243,9 +243,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
243243

244244
let onFocus = () => {
245245
enteredKeys.current = '';
246-
if (ref.current?.scrollIntoView) {
247-
ref.current.scrollIntoView();
248-
}
246+
scrollIntoView(getScrollParent(ref.current) as HTMLElement, ref.current);
249247

250248
// Safari requires that a selection is set or it won't fire input events.
251249
// Since usePress disables text selection, this won't happen by default.

packages/@react-aria/selection/src/useSelectableCollection.ts

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {FocusEvent, HTMLAttributes, Key, KeyboardEvent, RefObject, useEffect, useRef} from 'react';
1414
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
1515
import {FocusStrategy, KeyboardDelegate} from '@react-types/shared';
16-
import {focusWithoutScrolling, mergeProps, useEvent} from '@react-aria/utils';
16+
import {focusWithoutScrolling, mergeProps, scrollIntoView, useEvent} from '@react-aria/utils';
1717
import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
1818
import {MultipleSelectionManager} from '@react-stately/selection';
1919
import {useLocale} from '@react-aria/i18n';
@@ -402,57 +402,3 @@ export function useSelectableCollection(options: SelectableCollectionOptions): S
402402
}
403403
};
404404
}
405-
406-
/**
407-
* Scrolls `scrollView` so that `element` is visible.
408-
* Similar to `element.scrollIntoView({block: 'nearest'})` (not supported in Edge),
409-
* but doesn't affect parents above `scrollView`.
410-
*/
411-
function scrollIntoView(scrollView: HTMLElement, element: HTMLElement) {
412-
let offsetX = relativeOffset(scrollView, element, 'left');
413-
let offsetY = relativeOffset(scrollView, element, 'top');
414-
let width = element.offsetWidth;
415-
let height = element.offsetHeight;
416-
let x = scrollView.scrollLeft;
417-
let y = scrollView.scrollTop;
418-
let maxX = x + scrollView.offsetWidth;
419-
let maxY = y + scrollView.offsetHeight;
420-
421-
if (offsetX <= x) {
422-
x = offsetX;
423-
} else if (offsetX + width > maxX) {
424-
x += offsetX + width - maxX;
425-
}
426-
if (offsetY <= y) {
427-
y = offsetY;
428-
} else if (offsetY + height > maxY) {
429-
y += offsetY + height - maxY;
430-
}
431-
432-
scrollView.scrollLeft = x;
433-
scrollView.scrollTop = y;
434-
}
435-
436-
/**
437-
* Computes the offset left or top from child to ancestor by accumulating
438-
* offsetLeft or offsetTop through intervening offsetParents.
439-
*/
440-
function relativeOffset(ancestor: HTMLElement, child: HTMLElement, axis: 'left'|'top') {
441-
const prop = axis === 'left' ? 'offsetLeft' : 'offsetTop';
442-
let sum = 0;
443-
while (child.offsetParent) {
444-
sum += child[prop];
445-
if (child.offsetParent === ancestor) {
446-
// Stop once we have found the ancestor we are interested in.
447-
break;
448-
} else if (child.offsetParent.contains(ancestor)) {
449-
// If the ancestor is not `position:relative`, then we stop at
450-
// _its_ offset parent, and we subtract off _its_ offset, so that
451-
// we end up with the proper offset from child to ancestor.
452-
sum -= ancestor[prop];
453-
break;
454-
}
455-
child = child.offsetParent as HTMLElement;
456-
}
457-
return sum;
458-
}

packages/@react-aria/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ export * from './useDescription';
3232
export * from './platform';
3333
export * from './useEvent';
3434
export * from './useValueEffect';
35+
export * from './scrollIntoView';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2020 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
/**
14+
* Scrolls `scrollView` so that `element` is visible.
15+
* Similar to `element.scrollIntoView({block: 'nearest'})` (not supported in Edge),
16+
* but doesn't affect parents above `scrollView`.
17+
*/
18+
export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement) {
19+
let offsetX = relativeOffset(scrollView, element, 'left');
20+
let offsetY = relativeOffset(scrollView, element, 'top');
21+
let width = element.offsetWidth;
22+
let height = element.offsetHeight;
23+
let x = scrollView.scrollLeft;
24+
let y = scrollView.scrollTop;
25+
let maxX = x + scrollView.offsetWidth;
26+
let maxY = y + scrollView.offsetHeight;
27+
28+
if (offsetX <= x) {
29+
x = offsetX;
30+
} else if (offsetX + width > maxX) {
31+
x += offsetX + width - maxX;
32+
}
33+
if (offsetY <= y) {
34+
y = offsetY;
35+
} else if (offsetY + height > maxY) {
36+
y += offsetY + height - maxY;
37+
}
38+
39+
scrollView.scrollLeft = x;
40+
scrollView.scrollTop = y;
41+
}
42+
43+
/**
44+
* Computes the offset left or top from child to ancestor by accumulating
45+
* offsetLeft or offsetTop through intervening offsetParents.
46+
*/
47+
function relativeOffset(ancestor: HTMLElement, child: HTMLElement, axis: 'left'|'top') {
48+
const prop = axis === 'left' ? 'offsetLeft' : 'offsetTop';
49+
let sum = 0;
50+
while (child.offsetParent) {
51+
sum += child[prop];
52+
if (child.offsetParent === ancestor) {
53+
// Stop once we have found the ancestor we are interested in.
54+
break;
55+
} else if (child.offsetParent.contains(ancestor)) {
56+
// If the ancestor is not `position:relative`, then we stop at
57+
// _its_ offset parent, and we subtract off _its_ offset, so that
58+
// we end up with the proper offset from child to ancestor.
59+
sum -= ancestor[prop];
60+
break;
61+
}
62+
child = child.offsetParent as HTMLElement;
63+
}
64+
return sum;
65+
}

packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ storiesOf('Date and Time/DateRangePicker/styling', module)
160160
.add(
161161
'errorMessage',
162162
() => render({errorMessage: 'Dates must be after today', validationState: 'invalid'})
163+
)
164+
.add(
165+
'in scrollable container',
166+
() => (
167+
<div style={{height: '200vh'}}>
168+
{render({granularity: 'second'})}
169+
</div>
170+
)
163171
);
164172

165173
function render(props = {}) {

0 commit comments

Comments
 (0)