Skip to content

Commit a4dc30c

Browse files
committed
fix(cdk/overlay): simplify matching the overlay to the trigger width
The connected overlay directive allows users to control its width using the `cdkConnectedOverlayMatchWidth` input. This can be tricky when it needs to match another element like the trigger. Since this is a common for cases like dropdowns, these changes add the `cdkConnectedOverlayMatchWidth` input to simplify it. (cherry picked from commit 9332207)
1 parent 8d00344 commit a4dc30c

File tree

3 files changed

+42
-12
lines changed

3 files changed

+42
-12
lines changed

goldens/cdk/overlay/index.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
5555
hasBackdrop: boolean;
5656
height: number | string;
5757
lockPosition: boolean;
58+
matchWidth: boolean;
5859
minHeight: number | string;
5960
minWidth: number | string;
6061
// (undocumented)
@@ -68,6 +69,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
6869
// (undocumented)
6970
static ngAcceptInputType_lockPosition: unknown;
7071
// (undocumented)
72+
static ngAcceptInputType_matchWidth: unknown;
73+
// (undocumented)
7174
static ngAcceptInputType_push: unknown;
7275
// (undocumented)
7376
static ngAcceptInputType_usePopover: unknown;
@@ -95,7 +98,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
9598
viewportMargin: ViewportMargin;
9699
width: number | string;
97100
// (undocumented)
98-
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkConnectedOverlay, "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", ["cdkConnectedOverlay"], { "origin": { "alias": "cdkConnectedOverlayOrigin"; "required": false; }; "positions": { "alias": "cdkConnectedOverlayPositions"; "required": false; }; "positionStrategy": { "alias": "cdkConnectedOverlayPositionStrategy"; "required": false; }; "offsetX": { "alias": "cdkConnectedOverlayOffsetX"; "required": false; }; "offsetY": { "alias": "cdkConnectedOverlayOffsetY"; "required": false; }; "width": { "alias": "cdkConnectedOverlayWidth"; "required": false; }; "height": { "alias": "cdkConnectedOverlayHeight"; "required": false; }; "minWidth": { "alias": "cdkConnectedOverlayMinWidth"; "required": false; }; "minHeight": { "alias": "cdkConnectedOverlayMinHeight"; "required": false; }; "backdropClass": { "alias": "cdkConnectedOverlayBackdropClass"; "required": false; }; "panelClass": { "alias": "cdkConnectedOverlayPanelClass"; "required": false; }; "viewportMargin": { "alias": "cdkConnectedOverlayViewportMargin"; "required": false; }; "scrollStrategy": { "alias": "cdkConnectedOverlayScrollStrategy"; "required": false; }; "open": { "alias": "cdkConnectedOverlayOpen"; "required": false; }; "disableClose": { "alias": "cdkConnectedOverlayDisableClose"; "required": false; }; "transformOriginSelector": { "alias": "cdkConnectedOverlayTransformOriginOn"; "required": false; }; "hasBackdrop": { "alias": "cdkConnectedOverlayHasBackdrop"; "required": false; }; "lockPosition": { "alias": "cdkConnectedOverlayLockPosition"; "required": false; }; "flexibleDimensions": { "alias": "cdkConnectedOverlayFlexibleDimensions"; "required": false; }; "growAfterOpen": { "alias": "cdkConnectedOverlayGrowAfterOpen"; "required": false; }; "push": { "alias": "cdkConnectedOverlayPush"; "required": false; }; "disposeOnNavigation": { "alias": "cdkConnectedOverlayDisposeOnNavigation"; "required": false; }; "usePopover": { "alias": "cdkConnectedOverlayUsePopover"; "required": false; }; "_config": { "alias": "cdkConnectedOverlay"; "required": false; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
101+
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkConnectedOverlay, "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", ["cdkConnectedOverlay"], { "origin": { "alias": "cdkConnectedOverlayOrigin"; "required": false; }; "positions": { "alias": "cdkConnectedOverlayPositions"; "required": false; }; "positionStrategy": { "alias": "cdkConnectedOverlayPositionStrategy"; "required": false; }; "offsetX": { "alias": "cdkConnectedOverlayOffsetX"; "required": false; }; "offsetY": { "alias": "cdkConnectedOverlayOffsetY"; "required": false; }; "width": { "alias": "cdkConnectedOverlayWidth"; "required": false; }; "height": { "alias": "cdkConnectedOverlayHeight"; "required": false; }; "minWidth": { "alias": "cdkConnectedOverlayMinWidth"; "required": false; }; "minHeight": { "alias": "cdkConnectedOverlayMinHeight"; "required": false; }; "backdropClass": { "alias": "cdkConnectedOverlayBackdropClass"; "required": false; }; "panelClass": { "alias": "cdkConnectedOverlayPanelClass"; "required": false; }; "viewportMargin": { "alias": "cdkConnectedOverlayViewportMargin"; "required": false; }; "scrollStrategy": { "alias": "cdkConnectedOverlayScrollStrategy"; "required": false; }; "open": { "alias": "cdkConnectedOverlayOpen"; "required": false; }; "disableClose": { "alias": "cdkConnectedOverlayDisableClose"; "required": false; }; "transformOriginSelector": { "alias": "cdkConnectedOverlayTransformOriginOn"; "required": false; }; "hasBackdrop": { "alias": "cdkConnectedOverlayHasBackdrop"; "required": false; }; "lockPosition": { "alias": "cdkConnectedOverlayLockPosition"; "required": false; }; "flexibleDimensions": { "alias": "cdkConnectedOverlayFlexibleDimensions"; "required": false; }; "growAfterOpen": { "alias": "cdkConnectedOverlayGrowAfterOpen"; "required": false; }; "push": { "alias": "cdkConnectedOverlayPush"; "required": false; }; "disposeOnNavigation": { "alias": "cdkConnectedOverlayDisposeOnNavigation"; "required": false; }; "usePopover": { "alias": "cdkConnectedOverlayUsePopover"; "required": false; }; "matchWidth": { "alias": "cdkConnectedOverlayMatchWidth"; "required": false; }; "_config": { "alias": "cdkConnectedOverlay"; "required": false; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
99102
// (undocumented)
100103
static ɵfac: i0.ɵɵFactoryDeclaration<CdkConnectedOverlay, never>;
101104
}

src/cdk/overlay/overlay-directives.spec.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,19 @@ describe('Overlay directives', () => {
626626

627627
expect(target.style.transformOrigin).toContain('left bottom');
628628
});
629+
630+
it('should match the trigger width', () => {
631+
const trigger = fixture.nativeElement.querySelector('#trigger') as HTMLElement;
632+
trigger.style.width = '128px';
633+
634+
fixture.componentInstance.matchWidth = true;
635+
fixture.componentInstance.isOpen = true;
636+
fixture.changeDetectorRef.markForCheck();
637+
fixture.detectChanges();
638+
639+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
640+
expect(pane.style.width).toBe('128px');
641+
});
629642
});
630643

631644
describe('outputs', () => {
@@ -742,11 +755,11 @@ describe('Overlay directives', () => {
742755

743756
@Component({
744757
template: `
745-
<button cdk-overlay-origin id="trigger" #trigger="cdkOverlayOrigin">Toggle menu</button>
746-
<button cdk-overlay-origin id="otherTrigger" #otherTrigger="cdkOverlayOrigin">Toggle menu</button>
758+
<button cdkOverlayOrigin id="trigger" #trigger="cdkOverlayOrigin">Toggle menu</button>
759+
<button cdkOverlayOrigin id="otherTrigger" #otherTrigger="cdkOverlayOrigin">Toggle menu</button>
747760
<button id="nonDirectiveTrigger" #nonDirectiveTrigger>Toggle menu</button>
748761
749-
<ng-template cdk-connected-overlay
762+
<ng-template cdkConnectedOverlay
750763
[cdkConnectedOverlayOpen]="isOpen"
751764
[cdkConnectedOverlayWidth]="width"
752765
[cdkConnectedOverlayHeight]="height"
@@ -771,7 +784,8 @@ describe('Overlay directives', () => {
771784
[cdkConnectedOverlayMinWidth]="minWidth"
772785
[cdkConnectedOverlayMinHeight]="minHeight"
773786
[cdkConnectedOverlayPositions]="positionOverrides"
774-
[cdkConnectedOverlayTransformOriginOn]="transformOriginSelector">
787+
[cdkConnectedOverlayTransformOriginOn]="transformOriginSelector"
788+
[cdkConnectedOverlayMatchWidth]="matchWidth">
775789
<p>Menu content</p>
776790
</ng-template>`,
777791
imports: [OverlayModule],
@@ -809,12 +823,14 @@ class ConnectedOverlayDirectiveTest {
809823
detachHandler = jasmine.createSpy('detachHandler');
810824
attachResult: HTMLElement;
811825
transformOriginSelector: string;
826+
matchWidth = false;
812827
}
813828

814829
@Component({
815830
template: `
816-
<button cdk-overlay-origin #trigger="cdkOverlayOrigin">Toggle menu</button>
817-
<ng-template cdk-connected-overlay>Menu content</ng-template>`,
831+
<button cdkOverlayOrigin #trigger="cdkOverlayOrigin">Toggle menu</button>
832+
<ng-template cdk-connected-overlay>Menu content</ng-template>
833+
`,
818834
imports: [OverlayModule],
819835
})
820836
class ConnectedOverlayPropertyInitOrder {

src/cdk/overlay/overlay-directives.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
246246
@Input({alias: 'cdkConnectedOverlayUsePopover', transform: booleanAttribute})
247247
usePopover: boolean = false;
248248

249+
/** Whether the overlay should match the trigger's width. */
250+
@Input({alias: 'cdkConnectedOverlayMatchWidth', transform: booleanAttribute})
251+
matchWidth: boolean = false;
252+
249253
/** Shorthand for setting multiple overlay options at once. */
250254
@Input('cdkConnectedOverlay')
251255
set _config(value: string | CdkConnectedOverlayConfig) {
@@ -306,7 +310,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
306310
if (this._position) {
307311
this._updatePositionStrategy(this._position);
308312
this._overlayRef?.updateSize({
309-
width: this.width,
313+
width: this._getWidth(),
310314
minWidth: this.minWidth,
311315
height: this.height,
312316
minHeight: this.minHeight,
@@ -363,10 +367,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
363367
usePopover: this.usePopover,
364368
});
365369

366-
if (this.width || this.width === 0) {
367-
overlayConfig.width = this.width;
368-
}
369-
370370
if (this.height || this.height === 0) {
371371
overlayConfig.height = this.height;
372372
}
@@ -444,6 +444,15 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
444444
return null;
445445
}
446446

447+
private _getWidth() {
448+
if (this.width) {
449+
return this.width;
450+
}
451+
452+
// Null check `getBoundingClientRect` in case this is called during SSR.
453+
return this.matchWidth ? this._getOriginElement()?.getBoundingClientRect?.().width : undefined;
454+
}
455+
447456
/** Attaches the overlay. */
448457
attachOverlay() {
449458
if (!this._overlayRef) {
@@ -454,6 +463,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
454463

455464
// Update the overlay size, in case the directive's inputs have changed
456465
ref.getConfig().hasBackdrop = this.hasBackdrop;
466+
ref.updateSize({width: this._getWidth()});
457467

458468
if (!ref.hasAttached()) {
459469
ref.attach(this._templatePortal);
@@ -517,5 +527,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
517527
this.push = config.push ?? this.push;
518528
this.disposeOnNavigation = config.disposeOnNavigation ?? this.disposeOnNavigation;
519529
this.usePopover = config.usePopover ?? this.usePopover;
530+
this.matchWidth = config.matchWidth ?? this.matchWidth;
520531
}
521532
}

0 commit comments

Comments
 (0)