Skip to content

Commit

Permalink
Fix portal/popover position and scrolling issues (#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmslewis authored Dec 9, 2016
1 parent 41b3ae8 commit 82f35f3
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 8 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"dom4": "^1.8",
"normalize.css": "4.1.1",
"pure-render-decorator": "^1.1",
"tether": "^1.2"
"tether": "^1.4"
},
"peerDependencies": {
"react": "^15.0.1 || ^0.14",
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/common/tetherUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

// TODO: shim for new option added in Tether 1.4.0
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/13142
declare module "tether" {
interface ITetherOptions {
bodyElement?: HTMLElement;
}
}

import * as Tether from "tether";

import { Position } from "./position";
Expand All @@ -14,6 +22,15 @@ const DEFAULT_CONSTRAINTS = {
to: "scrollParent",
};

// per https://github.com/HubSpot/tether/pull/204, Tether now exposes a `bodyElement` option that,
// when present, gets the tethered element injected into *it* instead of into the document body.
// but both approaches still cause React to freak out, because it loses its handle on the DOM
// element. thus, we pass a fake HTML bodyElement to Tether, with a no-op `appendChild` function
// (the only function the library uses from bodyElement).
const fakeHtmlElement = ({
appendChild : () => { /* No-op */ },
} as any) as HTMLElement;

export interface ITetherConstraint {
attachment?: string;
outOfBoundsClass?: string;
Expand All @@ -34,6 +51,7 @@ export function createTetherOptions(element: Element,

const options: Tether.ITetherOptions = {
attachment: getPopoverAttachment(position),
bodyElement: fakeHtmlElement,
classPrefix: "pt-tether",
constraints,
element,
Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/components/overlay/overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,12 @@ export class Overlay extends React.Component<IOverlayProps, IOverlayState> {
const { children, className, inline, isOpen, transitionDuration, transitionName } = this.props;

// add a special class to each child that will automatically set the appropriate
// CSS position mode under the hood.
// CSS position mode under the hood. also, make the container focusable so we can
// trap focus inside it (via `persistentFocus()`).
const decoratedChildren = React.Children.map(children, (child: React.ReactElement<any>) => {
return React.cloneElement(child, {
className: classNames(child.props.className, Classes.OVERLAY_CONTENT),
tabIndex: 0,
});
});

Expand All @@ -188,8 +190,6 @@ export class Overlay extends React.Component<IOverlayProps, IOverlayState> {
const elementProps = {
className: mergedClassName,
onKeyDown: this.handleKeyDown,
// make the container focusable so we can trap focus inside it (via `persistentFocus()`)
tabIndex: 0,
};

if (inline) {
Expand Down Expand Up @@ -298,11 +298,12 @@ export class Overlay extends React.Component<IOverlayProps, IOverlayState> {
const isFocusOutsideModal = !containerElement.contains(document.activeElement);
if (isFocusOutsideModal) {
// element marked autofocus has higher priority than the other clowns
let autofocusElement = containerElement.query("[autofocus]") as HTMLElement;
const autofocusElement = containerElement.query("[autofocus]") as HTMLElement;
const wrapperElement = containerElement.query("[tabindex]") as HTMLElement;
if (autofocusElement != null) {
autofocusElement.focus();
} else {
containerElement.focus();
} else if (wrapperElement != null) {
wrapperElement.focus();
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/components/portal/_portal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,16 @@ The children of a `Portal` component are appended to the `<body>` element.
Styleguide components.portal.js
*/

.pt-portal {
// take the portal out of the document flow to prevent browsers from autoscrolling to the bottom
// of the document (where portals are appended) when programmatically focusing within a portal
// child element. also, don't use `fixed`, because then Tether'd elements won't reposition
// themselves properly as the document scrolls.
position: absolute;
// ensure content still offsets from the top of the document
top: 0;
// ensure content won't be horizontally scrunched
right: 0;
left: 0;
}
2 changes: 1 addition & 1 deletion packages/core/test/overlay/overlayTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe("<Overlay>", () => {
</Overlay>,
{ attachTo: testsContainerElement },
);
assert.equal(document.body.querySelector(".pt-overlay-open"), document.activeElement);
assert.equal(document.body.querySelector(".pt-overlay-backdrop"), document.activeElement);
});

it("does not bring focus to overlay if autoFocus=false", () => {
Expand Down

1 comment on commit 82f35f3

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix portal/popover position and scrolling issues (#326)

Preview: docs
Coverage: core | datetime

Please sign in to comment.