-
Notifications
You must be signed in to change notification settings - Fork 6.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(overlay): add support for flexible connected positioning #9153
Conversation
4b04504
to
578f5f4
Compare
I'm excited for this to land! Thanks for the work @crisbeto. |
src/cdk/overlay/_overlay.scss
Outdated
|
||
// For connected-position overlays, we set `display: flex` in order to force `max-width` | ||
// and `max-height` to take effect. | ||
// todo: make sure this doesn't break existing stuff |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this todo
be removed?
.connectedTo(this.origin.elementRef, originPoint, overlayPoint) | ||
.withOffsetX(this.offsetX) | ||
.withOffsetY(this.offsetY); | ||
.flexibleConnectedTo(this.origin.elementRef) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment that explains why these features are turned off?
src/cdk/overlay/overlay-ref.ts
Outdated
@@ -27,6 +27,7 @@ export type ImmutableObject<T> = { | |||
*/ | |||
export class OverlayRef implements PortalOutlet { | |||
private _backdropElement: HTMLElement | null = null; | |||
private _wrapper: HTMLElement | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you think of any uses for this wrapper element other than positioning?
It doesn't feel quite right that the OverlayRef
has the responsibility for creating the wrapper, since the ref is inherently tied to a specific element at creation. I'm thinking that Overlay.create
should create the element and we make it a fixed assumption going forward that the parentNode
of the pane is this wrapping element. I'd also like to use a different term than wrapper; what do you think of "host"? If we decide that this really is only meant for positioning, then the structure would be
OverlayContainer
holds all overlay content- Each overlay is inside of its own
positioningHost
- Each overlay still corresponds 1:1 with a panel element inside the positioning host
I'm open to different terms but can't think of a better one presently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wrapper is only used for positioning. Initially I wanted to keep it inside the position strategy (the same way we had it for the GlobalPositionStrategy
), but it becomes tricky when we start detaching and re-attaching the same overlay. I made it opt-in to avoid changing the current behavior too much, but it should be fine since the wrapper won't do anything if it doesn't have any associated CSS.
src/cdk/overlay/overlay-ref.ts
Outdated
@@ -51,6 +53,11 @@ export class OverlayRef implements PortalOutlet { | |||
return this._pane; | |||
} | |||
|
|||
/** Wrapper around the panel element. */ | |||
get overlayWrapper(): HTMLElement | null { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrapperElement
?
It would also be good if the API docs gave some indication as to what the wrapper should generally be used for.
// TODO: refactor clipping detection into a separate thing (part of scrolling module) | ||
// TODO: attribute selector to specify the transform-origin inside the overlay content | ||
// TODO: flexible position + centering doesn't work on IE11 (works on Edge). | ||
// TODO: doesn't handle both flexible width and height when it has to scroll along both axis. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these TODOs still relevant?
|
||
/** Creates an absolutely positioned, display: block element with a default size. */ | ||
function createPositionedBlockElement() { | ||
let element = createBlockElement(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const
here and in the other helper functions?
|
||
const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); | ||
expect(Math.floor(overlayRect.height)).toBe(OVERLAY_HEIGHT - 10); | ||
expect(Math.floor(overlayRect.bottom)).toBe(viewport.getViewportSize().height); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this test also check scrollHeight
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, we're positioning relative to the viewport, not the page.
private _resizeSubscription = Subscription.EMPTY; | ||
|
||
/** Observable sequence of position changes. */ | ||
positionChange: Observable<ConnectedOverlayPositionChange> = this._positionChange.asObservable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
positionChanges
?
578f5f4
to
d70be08
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworked it based on the feedback @jelbourn.
src/cdk/overlay/overlay-ref.ts
Outdated
@@ -27,6 +27,7 @@ export type ImmutableObject<T> = { | |||
*/ | |||
export class OverlayRef implements PortalOutlet { | |||
private _backdropElement: HTMLElement | null = null; | |||
private _wrapper: HTMLElement | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wrapper is only used for positioning. Initially I wanted to keep it inside the position strategy (the same way we had it for the GlobalPositionStrategy
), but it becomes tricky when we start detaching and re-attaching the same overlay. I made it opt-in to avoid changing the current behavior too much, but it should be fine since the wrapper won't do anything if it doesn't have any associated CSS.
d70be08
to
ffe7f09
Compare
// If there are any positions where the overlay would fit with flexible dimensions, choose the | ||
// one that has the greatest area available modified by the position's weight | ||
if (flexibleFits.length) { | ||
let bestFit: FlexibleFit | null = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just set to flexibleFits[0]
initially? that way we don't need bestFit!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that it's the same functionality. If we did that, the first fit would have a score of -1 (unless we also moved out the score calculation) which would make it impossible to select.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you'd obviously need to set the initial score correctly as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That means moving that long line outside the loop though.
this._boundingBox = overlayRef.hostElement!; | ||
this._pane = overlayRef.overlayElement; | ||
this._resizeSubscription.unsubscribe(); | ||
this._resizeSubscription = this._viewportRuler.change().subscribe(() => this.apply()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit/optional: could use takeUntil(this._detached)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to do it inside the disposed
and I feel like introducing another Subject
for this one case would be overkill.
private _isInitialRender = true; | ||
|
||
/** Last height used for the bounding box. Used to avoid resizing the overlay after open. */ | ||
private _lastBoundingBoxHeight = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit/optional: _lastBoundingBoxSize: {width: number, height: number}
then you can directly do
= ...getBoundingClientRect()
private _lastPosition: ConnectedPosition; | ||
|
||
/** Subject that emits whenever the position changes. */ | ||
private _positionChanges = new Subject<ConnectedOverlayPositionChange>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.complete()
this in dispose
if (this._canPush) { | ||
// TODO(jelbourn): after pushing, the opening "direction" of the overlay might not make sense. | ||
this._isPushed = true; | ||
this._applyPosition(fallback!.position, fallback!.originPoint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fallback
actually could be undefined at this point, are we sure we want to just throw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. It should've been assigned here, unless we exited already https://github.com/angular/material2/pull/9153/files#diff-be0f6cfbf065a6175e266e5bc8cd5213R219.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like its only set inside that for loop, so if this._perferredPositions == []
it would not be set
/** Cleanup after the element gets destroyed. */ | ||
dispose() { | ||
this._boundingBox = null; | ||
this._resizeSubscription.unsubscribe(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call detach
to make sure we do all detaching stuff, whatever that migth be
|
||
/** | ||
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position. | ||
* @param originRect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
param descriptions (or just omit @param
)
let transformString = ''; | ||
|
||
if (position.offsetX) { | ||
transformString += `translateX(${position.offsetX}px)`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefix with ' '
to prevent potential future bugs (the trim
will just remove it anyways). or use an array and .join(' ')
ffe7f09
to
1b61bd8
Compare
I ran an early presubmit on this and it resulted in a fair number of failures. I'll try to look into them today to figure out if it's just their tests or something in the PR. (I suspect it's their tests depending on a specific DOM structure) |
FWIW, I was waiting for the final review to look into a couple of unit test failures on Safari, but IIRC they were only for the centered position + offset case which isn't super common. |
Presubmit failure number 1: one app is somehow getting into a state where a call to Adding an |
1b61bd8
to
541684a
Compare
Sorted out the |
@crisbeto needs rebase |
Summary of remaining failures:
I'll try to debug these tomorrow, but you might have ideas in the meantime. |
The first two sound like something is definitely wrong on our end, but I'm not sure what it would be off the top of my head. As far as I remember the logic for a non-flexible overlay is more or less the same. I've also ported all the unit tests from the old positioning into the new one and they passed with zero changes. |
Turns out that first one was actually a select. It was caused by the old version of select we have in Google (called mat-deprecated-select) that some teams are still using. This was leftover from when we switched select to be inside form-field. I was able to update the old code to resolve the issue (the |
So, for the menu being slightly off, I think I found what's wrong. The case is where the menu-trigger is close to the right side of the screen. The overlay-container is set to We probably don't want to change overlay-container since it would likely affect downstream apps. We should be able to manually account for the scrollbar in the calculations. We could alternatively use |
541684a
to
3726d81
Compare
The |
Next failure: since the bounding box starts off with no size, |
3726d81
to
63deb95
Compare
Done @jelbourn. |
What's the status of this? There have been several releases since the last comment. Every time there's a release, I rush to check the changelog for this fix and it still isn't merged in. Are there technical concerns with the implementation? |
63deb95
to
65b76cb
Compare
* Adds the `FlexibleConnectedPositionStrategy` that builds on top of the `ConnectedPositionStrategy` adds the following: * The ability to have overlays with flexible sizing. * Being able to push overlays into the viewport if they don't fit. * Having a margin between the overlay and the viewport edge. * Being able to assign weights to the overlay positions. * Refactors the `ConnectedPositionStrategy` to use the `FlexibleConnectedPositionStrategy` in order to avoid breaking API changes. * Switches all of the components to the new position strategy. * Adds an API to the `OverlayRef` that allows for the consumer to wrap the pane in a div. This is a common requirement between the `GlobalPositionStrategy` and the `FlexibleConnectedPositionStrategy`, and it's easier to keep track of the elements when the attaching and detaching is done in the `OverlayRef`. Fixes angular#6534. Fixes angular#2725. Fixes angular#5267.
65b76cb
to
3a9965c
Compare
Something that was done initially in angular#9153, but ended up being re-added after a large rebase.
…r#9153) * Adds the `FlexibleConnectedPositionStrategy` that builds on top of the `ConnectedPositionStrategy` adds the following: * The ability to have overlays with flexible sizing. * Being able to push overlays into the viewport if they don't fit. * Having a margin between the overlay and the viewport edge. * Being able to assign weights to the overlay positions. * Refactors the `ConnectedPositionStrategy` to use the `FlexibleConnectedPositionStrategy` in order to avoid breaking API changes. * Switches all of the components to the new position strategy. * Adds an API to the `OverlayRef` that allows for the consumer to wrap the pane in a div. This is a common requirement between the `GlobalPositionStrategy` and the `FlexibleConnectedPositionStrategy`, and it's easier to keep track of the elements when the attaching and detaching is done in the `OverlayRef`. Fixes angular#6534. Fixes angular#2725. Fixes angular#5267.
Something that was done initially in #9153, but ended up being re-added after a large rebase.
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
FlexibleConnectedPositionStrategy
that builds on top of theConnectedPositionStrategy
adds the following:ConnectedPositionStrategy
to use theFlexibleConnectedPositionStrategy
in order to avoid breaking API changes.OverlayRef
that allows for the consumer to wrap the pane in a div. This is a common requirement between theGlobalPositionStrategy
and theFlexibleConnectedPositionStrategy
, and it's easier to keep track of the elements when the attaching and detaching is done in theOverlayRef
.Fixes #6534.
Fixes #2725.
Fixes #5267.