Skip to content

Commit 09b882e

Browse files
committed
feat(floating-menu): more robust floating menu offset
1 parent c787226 commit 09b882e

File tree

5 files changed

+130
-12
lines changed

5 files changed

+130
-12
lines changed

packages/react/src/components/OverflowMenu/OverflowMenu-story.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,40 @@ storiesOf('OverflowMenu', module)
140140
`,
141141
},
142142
}
143+
)
144+
.add(
145+
'custom viewport',
146+
withReadme(OverflowREADME, () => (
147+
<div
148+
id="overflow-menu-custom-viewport-container"
149+
style={{
150+
border: '1px solid black',
151+
width: 400,
152+
height: 400,
153+
overflow: 'scroll',
154+
position: 'absolute',
155+
top: 200,
156+
left: 200,
157+
}}>
158+
<div data-floating-menu-container>
159+
<OverflowMenuExample
160+
overflowMenuProps={{
161+
...props.menu(),
162+
getViewport: () =>
163+
document.getElementById(
164+
'overflow-menu-custom-viewport-container'
165+
),
166+
}}
167+
overflowMenuItemProps={props.menuItem()}
168+
/>
169+
</div>
170+
</div>
171+
)),
172+
{
173+
info: {
174+
text: `
175+
A custom viewport can be specified to make sure that the menu is offset correctly when the floating menu container is within some absolutely positioned element with it's own scrolling behavior.
176+
`,
177+
},
178+
}
143179
);

packages/react/src/components/OverflowMenu/OverflowMenu.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ class OverflowMenu extends Component {
203203
* Function called when menu is closed
204204
*/
205205
onOpen: PropTypes.func,
206+
207+
/**
208+
* Optional callback used to obtain a custom 'viewport' that differs from the window.
209+
*/
210+
getViewport: PropTypes.func,
206211
};
207212

208213
static defaultProps = {
@@ -453,6 +458,7 @@ class OverflowMenu extends Component {
453458
renderIcon: IconElement,
454459
innerRef: ref,
455460
menuOptionsClass,
461+
getViewport,
456462
...other
457463
} = this.props;
458464

@@ -507,6 +513,7 @@ class OverflowMenu extends Component {
507513
menuPosition={this.state.menuPosition}
508514
menuDirection={direction}
509515
menuOffset={flipped ? menuOffsetFlip : menuOffset}
516+
getViewport={getViewport}
510517
menuRef={this._bindMenuBody}
511518
menuEl={this.menuEl}
512519
flipped={this.props.flipped}

packages/react/src/components/Tooltip/Tooltip-story.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,51 @@ storiesOf('Tooltip', module)
202202
},
203203
}
204204
)
205-
.add('uncontrolled tooltip', () => <UncontrolledTooltipExample />);
205+
.add('uncontrolled tooltip', () => <UncontrolledTooltipExample />)
206+
.add(
207+
'custom viewport',
208+
() => (
209+
<div
210+
id="overflow-menu-custom-viewport-container"
211+
style={{
212+
border: '1px solid black',
213+
width: 400,
214+
height: 400,
215+
overflow: 'scroll',
216+
position: 'absolute',
217+
top: 200,
218+
left: 200,
219+
}}>
220+
<div data-floating-menu-container>
221+
<div style={{ marginTop: '2rem' }}>
222+
<Tooltip
223+
{...props.withIcon()}
224+
getViewport={() =>
225+
document.getElementById(
226+
'overflow-menu-custom-viewport-container'
227+
)
228+
}>
229+
<p>
230+
This is some tooltip text. This box shows the maximum amount of
231+
text that should appear inside. If more room is needed please
232+
use a modal instead.
233+
</p>
234+
<div className={`${prefix}--tooltip__footer`}>
235+
<a href="/" className={`${prefix}--link`}>
236+
Learn More
237+
</a>
238+
<Button size="small">Create</Button>
239+
</div>
240+
</Tooltip>
241+
</div>
242+
</div>
243+
</div>
244+
),
245+
{
246+
info: {
247+
text: `
248+
A custom viewport can be specified to make sure that the menu is offset correctly when the floating menu container is within some absolutely positioned element with it's own scrolling behavior.
249+
`,
250+
},
251+
}
252+
);

packages/react/src/components/Tooltip/Tooltip.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ class Tooltip extends Component {
188188
onChange: !useControlledStateWithValue
189189
? PropTypes.func
190190
: requiredIfValueExists(PropTypes.func),
191+
192+
/**
193+
* Optional callback used to obtain a custom 'viewport' that differs from the window.
194+
*/
195+
getViewport: PropTypes.func,
191196
};
192197

193198
static defaultProps = {
@@ -379,6 +384,7 @@ class Tooltip extends Component {
379384
menuOffset,
380385
tabIndex = 0,
381386
innerRef: ref,
387+
getViewport,
382388
...other
383389
} = this.props;
384390

@@ -451,6 +457,7 @@ class Tooltip extends Component {
451457
menuPosition={this.state.triggerPosition}
452458
menuDirection={direction}
453459
menuOffset={menuOffset}
460+
getViewport={getViewport}
454461
menuRef={node => {
455462
this._tooltipEl = node;
456463
}}>

packages/react/src/internal/FloatingMenu.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const hasChangeInOffset = (oldMenuOffset = {}, menuOffset = {}) => {
6565
* @param {FloatingMenu~size} params.menuSize The size of the menu.
6666
* @param {FloatingMenu~position} params.refPosition The position of the triggering element.
6767
* @param {FloatingMenu~offset} [params.offset={ left: 0, top: 0 }] The position offset of the menu.
68+
* @param {FloatingMenu~offset} [params.viewportOffset={left: 0, top: 0}] The position offset of the custom viewport
6869
* @param {string} [params.direction=bottom] The menu direction.
6970
* @param {number} [params.scrollX=0] The scroll position of the viewport.
7071
* @param {number} [params.scrollY=0] The scroll position of the viewport.
@@ -75,6 +76,7 @@ const getFloatingPosition = ({
7576
menuSize,
7677
refPosition,
7778
offset = {},
79+
viewportOffset = {},
7880
direction = DIRECTION_BOTTOM,
7981
scrollX = 0,
8082
scrollY = 0,
@@ -88,25 +90,26 @@ const getFloatingPosition = ({
8890

8991
const { width, height } = menuSize;
9092
const { top = 0, left = 0 } = offset;
93+
const { left: viewportLeft = 0, top: viewportTop = 0 } = viewportOffset;
9194
const refCenterHorizontal = (refLeft + refRight) / 2;
9295
const refCenterVertical = (refTop + refBottom) / 2;
9396

9497
return {
9598
[DIRECTION_LEFT]: () => ({
96-
left: refLeft - width + scrollX - left,
97-
top: refCenterVertical - height / 2 + scrollY + top,
99+
left: refLeft - width + scrollX - left - viewportLeft,
100+
top: refCenterVertical - height / 2 + scrollY + top - viewportTop,
98101
}),
99102
[DIRECTION_TOP]: () => ({
100-
left: refCenterHorizontal - width / 2 + scrollX + left,
101-
top: refTop - height + scrollY - top,
103+
left: refCenterHorizontal - width / 2 + scrollX + left - viewportLeft,
104+
top: refTop - height + scrollY - top - viewportTop,
102105
}),
103106
[DIRECTION_RIGHT]: () => ({
104-
left: refRight + scrollX + left,
105-
top: refCenterVertical - height / 2 + scrollY + top,
107+
left: refRight + scrollX + left - viewportLeft,
108+
top: refCenterVertical - height / 2 + scrollY + top - viewportTop,
106109
}),
107110
[DIRECTION_BOTTOM]: () => ({
108-
left: refCenterHorizontal - width / 2 + scrollX + left,
109-
top: refBottom + scrollY + top,
111+
left: refCenterHorizontal - width / 2 + scrollX + left - viewportLeft,
112+
top: refBottom + scrollY + top - viewportTop,
110113
}),
111114
}[direction]();
112115
};
@@ -172,6 +175,11 @@ class FloatingMenu extends React.Component {
172175
* The callback called when the menu body has been mounted and positioned.
173176
*/
174177
onPlace: PropTypes.func,
178+
179+
/**
180+
* Optional callback used to obtain a custom 'viewport' that differs from the window.
181+
*/
182+
getViewport: PropTypes.func,
175183
};
176184

177185
static defaultProps = {
@@ -233,11 +241,13 @@ class FloatingMenu extends React.Component {
233241
menuPosition: oldRefPosition = {},
234242
menuOffset: oldMenuOffset = {},
235243
menuDirection: oldMenuDirection,
244+
getViewport: oldGetViewport,
236245
} = prevProps;
237246
const {
238247
menuPosition: refPosition = {},
239248
menuOffset = {},
240249
menuDirection,
250+
getViewport,
241251
} = this.props;
242252

243253
if (
@@ -246,26 +256,37 @@ class FloatingMenu extends React.Component {
246256
oldRefPosition.bottom !== refPosition.bottom ||
247257
oldRefPosition.left !== refPosition.left ||
248258
hasChangeInOffset(oldMenuOffset, menuOffset) ||
249-
oldMenuDirection !== menuDirection
259+
oldMenuDirection !== menuDirection ||
260+
(oldGetViewport && oldGetViewport()) !== (getViewport && getViewport())
250261
) {
251262
const menuSize = menuBody.getBoundingClientRect();
252263
const { menuEl, flipped } = this.props;
253264
const offset =
254265
typeof menuOffset !== 'function'
255266
? menuOffset
256267
: menuOffset(menuBody, menuDirection, menuEl, flipped);
268+
269+
const viewport = getViewport && getViewport();
270+
const viewportSize = viewport && viewport.getBoundingClientRect();
257271
// Skips if either in the following condition:
258272
// a) Menu body has `display:none`
259273
// b) `menuOffset` as a callback returns `undefined` (The callback saw that it couldn't calculate the value)
274+
260275
if ((menuSize.width > 0 && menuSize.height > 0) || !offset) {
261276
this.setState({
262277
floatingPosition: getFloatingPosition({
263278
menuSize,
264279
refPosition,
265280
direction: menuDirection,
266281
offset,
267-
scrollX: window.pageXOffset,
268-
scrollY: window.pageYOffset,
282+
viewportOffset: {
283+
...(viewportSize && {
284+
left: viewportSize.left,
285+
top: viewportSize.top,
286+
}),
287+
},
288+
scrollX: viewport ? viewport.scrollLeft : window.pageXOffset,
289+
scrollY: viewport ? viewport.scrollTop : window.pageYOffset,
269290
}),
270291
});
271292
}

0 commit comments

Comments
 (0)