Skip to content

Commit e5d6994

Browse files
filipw01snowystingerLFDanLu
authored
@react-aria/overlays - Fix overlay position clamping (#3756)
* Fix overlay position clamping * Add tests and update test data Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Daniel Lu <[email protected]>
1 parent 7f69e65 commit e5d6994

File tree

3 files changed

+59
-29
lines changed

3 files changed

+59
-29
lines changed

packages/@react-aria/overlays/src/calculatePosition.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';
14+
import {clamp} from '@react-aria/utils';
1415

1516
interface Position {
1617
top?: number,
@@ -202,13 +203,16 @@ function computePosition(
202203
// add the crossOffset from props
203204
position[crossAxis] += crossOffset;
204205

205-
// this is button center position - the overlay size + half of the button to align bottom of overlay with button center
206-
let minViablePosition = childOffset[crossAxis] + (childOffset[crossSize] / 2) - overlaySize[crossSize];
207-
// this is button position of center, aligns top of overlay with button center
208-
let maxViablePosition = childOffset[crossAxis] + (childOffset[crossSize] / 2);
206+
// button bottom touching overlay bottom
207+
let bottomsTouchPosition = childOffset[crossAxis] + childOffset[crossSize] - overlaySize[crossSize];
208+
// button top touching overlay top
209+
let topsTouchPosition = childOffset[crossAxis];
209210

210-
// clamp it into the range of the min/max positions
211-
position[crossAxis] = Math.min(Math.max(minViablePosition, position[crossAxis]), maxViablePosition);
211+
// we can't assume which one is bigger because button could be bigger than overlay and vice versa
212+
const minPosition = Math.min(bottomsTouchPosition, topsTouchPosition);
213+
const maxPosition = Math.max(bottomsTouchPosition, topsTouchPosition);
214+
215+
position[crossAxis] = clamp(position[crossAxis], minPosition, maxPosition);
212216

213217
// Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.
214218
if (placement === axis) {

packages/@react-aria/overlays/stories/UseOverlayPosition.stories.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import {useOverlayTriggerState} from '@react-stately/overlays';
2121
function Trigger(props: {
2222
withPortal: boolean,
2323
placement: Placement,
24-
maxHeight?: number
24+
maxHeight?: number,
25+
buttonWidth?: number
2526
}) {
26-
const {withPortal, placement, maxHeight} = props;
27+
const {withPortal, placement, maxHeight, buttonWidth} = props;
2728
const targetRef = React.useRef<HTMLButtonElement>(null);
2829
const overlayRef = React.useRef<HTMLDivElement>(null);
2930
const state = useOverlayTriggerState({
@@ -71,7 +72,13 @@ function Trigger(props: {
7172
}
7273
return (
7374
<div style={{position: 'relative', margin: 'auto'}}>
74-
<button ref={targetRef} {...triggerProps} onClick={() => state.toggle()}>Trigger (open: {`${state.isOpen}`})</button>
75+
<button
76+
ref={targetRef}
77+
{...triggerProps}
78+
style={{width: buttonWidth}}
79+
onClick={() => state.toggle()}>
80+
Trigger (open: {`${state.isOpen}`})
81+
</button>
7582
{state.isOpen && overlay}
7683
</div>
7784
);
@@ -82,5 +89,6 @@ storiesOf('UseOverlayPosition', module)
8289
.add('document.body container top', () => <Trigger withPortal placement="top" />)
8390
.add('positioned container bottom', () => <Trigger withPortal={false} placement="bottom" />)
8491
.add('positioned container top', () => <Trigger withPortal={false} placement="top" />)
92+
.add('buttonWidth=500 document.body bottom start', () => <Trigger withPortal buttonWidth={500} placement="bottom start" />)
8593
.add('maxHeight=200 container bottom', () => <Trigger withPortal maxHeight={200} placement="bottom" />)
8694
.add('maxHeight=200 container top', () => <Trigger withPortal maxHeight={200} placement="top" />);

packages/@react-aria/overlays/test/calculatePosition.test.js

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -194,95 +194,107 @@ describe('calculatePosition', function () {
194194
noOffset: [50, 200, undefined, 100, 350],
195195
offsetBefore: [-200, 50, undefined, 0, 500],
196196
offsetAfter: [300, 350, undefined, 200, 200],
197-
crossAxisOffset: [50, 210, undefined, 90, 340],
197+
crossAxisOffsetPositive: [50, 210, undefined, 90, 340],
198+
crossAxisOffsetNegative: [50, 190, undefined, 110, 360],
198199
mainAxisOffset: [40, 200, undefined, 100, 350]
199200
},
200201
{
201202
placement: 'left top',
202203
noOffset: [50, 250, undefined, 50, 300],
203204
offsetBefore: [-200, 50, undefined, 0, 500],
204205
offsetAfter: [300, 350, undefined, 200, 200],
205-
crossAxisOffset: [50, 260, undefined, 40, 290],
206+
crossAxisOffsetPositive: [50, 250, undefined, 50, 300],
207+
crossAxisOffsetNegative: [50, 240, undefined, 60, 310],
206208
mainAxisOffset: [40, 250, undefined, 50, 300]
207209
},
208210
{
209211
placement: 'left bottom',
210212
noOffset: [50, 150, undefined, 150, 400],
211213
offsetBefore: [-200, 50, undefined, 0, 500],
212214
offsetAfter: [300, 350, undefined, 200, 200],
213-
crossAxisOffset: [50, 160, undefined, 140, 390],
215+
crossAxisOffsetPositive: [50, 160, undefined, 140, 390],
216+
crossAxisOffsetNegative: [50, 150, undefined, 150, 400],
214217
mainAxisOffset: [40, 150, undefined, 150, 400]
215218
},
216219
{
217220
placement: 'top',
218221
noOffset: [200, 50, 100, undefined, 200],
219222
offsetBefore: [50, -200, 0, undefined, 0],
220223
offsetAfter: [350, 300, 200, undefined, 450],
221-
mainAxisOffset: [200, 40, 100, undefined, 200],
222-
crossAxisOffset: [210, 50, 90, undefined, 200]
224+
crossAxisOffsetPositive: [210, 50, 90, undefined, 200],
225+
crossAxisOffsetNegative: [190, 50, 110, undefined, 200],
226+
mainAxisOffset: [200, 40, 100, undefined, 200]
223227
},
224228
{
225229
placement: 'top left',
226230
noOffset: [250, 50, 50, undefined, 200],
227231
offsetBefore: [50, -200, 0, undefined, 0],
228232
offsetAfter: [350, 300, 200, undefined, 450],
229-
mainAxisOffset: [250, 40, 50, undefined, 200],
230-
crossAxisOffset: [260, 50, 40, undefined, 200]
233+
crossAxisOffsetPositive: [250, 50, 50, undefined, 200],
234+
crossAxisOffsetNegative: [240, 50, 60, undefined, 200],
235+
mainAxisOffset: [250, 40, 50, undefined, 200]
231236
},
232237
{
233238
placement: 'top right',
234239
noOffset: [150, 50, 150, undefined, 200],
235240
offsetBefore: [50, -200, 0, undefined, 0],
236241
offsetAfter: [350, 300, 200, undefined, 450],
237-
mainAxisOffset: [150, 40, 150, undefined, 200],
238-
crossAxisOffset: [160, 50, 140, undefined, 200]
242+
crossAxisOffsetPositive: [160, 50, 140, undefined, 200],
243+
crossAxisOffsetNegative: [150, 50, 150, undefined, 200],
244+
mainAxisOffset: [150, 40, 150, undefined, 200]
239245
},
240246
{
241247
placement: 'bottom',
242248
noOffset: [200, 350, 100, undefined, 200],
243249
offsetBefore: [50, 100, 0, undefined, 450],
244250
offsetAfter: [350, 600, 200, undefined, 0],
245-
mainAxisOffset: [200, 360, 100, undefined, 190],
246-
crossAxisOffset: [210, 350, 90, undefined, 200]
251+
crossAxisOffsetPositive: [210, 350, 90, undefined, 200],
252+
crossAxisOffsetNegative: [190, 350, 110, undefined, 200],
253+
mainAxisOffset: [200, 360, 100, undefined, 190]
247254
},
248255
{
249256
placement: 'bottom left',
250257
noOffset: [250, 350, 50, undefined, 200],
251258
offsetBefore: [50, 100, 0, undefined, 450],
252259
offsetAfter: [350, 600, 200, undefined, 0],
253-
mainAxisOffset: [250, 360, 50, undefined, 190],
254-
crossAxisOffset: [260, 350, 40, undefined, 200]
260+
crossAxisOffsetPositive: [250, 350, 50, undefined, 200],
261+
crossAxisOffsetNegative: [240, 350, 60, undefined, 200],
262+
mainAxisOffset: [250, 360, 50, undefined, 190]
255263
},
256264
{
257265
placement: 'bottom right',
258266
noOffset: [150, 350, 150, undefined, 200],
259267
offsetBefore: [50, 100, 0, undefined, 450],
260268
offsetAfter: [350, 600, 200, undefined, 0],
261-
mainAxisOffset: [150, 360, 150, undefined, 190],
262-
crossAxisOffset: [160, 350, 140, undefined, 200]
269+
crossAxisOffsetPositive: [160, 350, 140, undefined, 200],
270+
crossAxisOffsetNegative: [150, 350, 150, undefined, 200],
271+
mainAxisOffset: [150, 360, 150, undefined, 190]
263272
},
264273
{
265274
placement: 'right',
266275
noOffset: [350, 200, undefined, 100, 350],
267276
offsetBefore: [100, 50, undefined, 0, 500],
268277
offsetAfter: [600, 350, undefined, 200, 200],
269-
crossAxisOffset: [350, 210, undefined, 90, 340],
278+
crossAxisOffsetPositive: [350, 210, undefined, 90, 340],
279+
crossAxisOffsetNegative: [350, 190, undefined, 110, 360],
270280
mainAxisOffset: [360, 200, undefined, 100, 350]
271281
},
272282
{
273283
placement: 'right top',
274284
noOffset: [350, 250, undefined, 50, 300],
275285
offsetBefore: [100, 50, undefined, 0, 500],
276286
offsetAfter: [600, 350, undefined, 200, 200],
277-
crossAxisOffset: [350, 260, undefined, 40, 290],
287+
crossAxisOffsetPositive: [350, 250, undefined, 50, 300],
288+
crossAxisOffsetNegative: [350, 240, undefined, 60, 310],
278289
mainAxisOffset: [360, 250, undefined, 50, 300]
279290
},
280291
{
281292
placement: 'right bottom',
282293
noOffset: [350, 150, undefined, 150, 400],
283294
offsetBefore: [100, 50, undefined, 0, 500],
284295
offsetAfter: [600, 350, undefined, 200, 200],
285-
crossAxisOffset: [350, 160, undefined, 140, 390],
296+
crossAxisOffsetPositive: [350, 160, undefined, 140, 390],
297+
crossAxisOffsetNegative: [350, 150, undefined, 150, 400],
286298
mainAxisOffset: [360, 150, undefined, 150, 400]
287299
}
288300
];
@@ -321,9 +333,15 @@ describe('calculatePosition', function () {
321333
);
322334
});
323335

324-
describe('cross axis offset', function () {
336+
describe('cross axis offset positive', function () {
325337
checkPosition(
326-
placement, getTargetDimension({left: 250, top: 250}), testCase.crossAxisOffset, 0, 10
338+
placement, getTargetDimension({left: 250, top: 250}), testCase.crossAxisOffsetPositive, 0, 10
339+
);
340+
});
341+
342+
describe('cross axis offset negative', function () {
343+
checkPosition(
344+
placement, getTargetDimension({left: 250, top: 250}), testCase.crossAxisOffsetNegative, 0, -10
327345
);
328346
});
329347
});

0 commit comments

Comments
 (0)