Skip to content

Commit

Permalink
fix(material/menu): account for menu padding different from the default
Browse files Browse the repository at this point in the history
Currently we use the hardcoded value of the menu padding to offset the panel so the two menu triggers align. Since the value is in TS, the logic breaks down if the user has set their own padding. These changes add some logic to compute the padding the first time a sub-menu is opened.

Fixes angular#16167.
  • Loading branch information
crisbeto committed Feb 27, 2022
1 parent 5fc655b commit 55c7726
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 21 deletions.
7 changes: 6 additions & 1 deletion src/material-experimental/mdc-menu/menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ mat-menu {
@include menu-common.base;

// The CDK positioning uses flexbox to anchor the element, whereas MDC uses `position: absolute`.
position: static;
// Furthermore, the relative position ensures that the `offsetParent` is the menu, rather than
// the overlay. This comes into play when we measure the `offsetTop` of a menu item.
position: relative;
top: 0;
left: 0;
transform: none;
}

.mat-mdc-menu-item {
Expand Down
36 changes: 28 additions & 8 deletions src/material-experimental/mdc-menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2137,7 +2137,7 @@ describe('MDC-based MatMenu', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '50px';
instance.rootTriggerEl.nativeElement.style.top = '50px';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2147,7 +2147,7 @@ describe('MDC-based MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-mdc-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.right)).toBe(Math.round(panelRect.left));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
Expand All @@ -2157,7 +2157,7 @@ describe('MDC-based MatMenu', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.right = '10px';
instance.rootTriggerEl.nativeElement.style.top = '50%';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2167,7 +2167,7 @@ describe('MDC-based MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-mdc-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.left)).toBe(Math.round(panelRect.right));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
Expand All @@ -2177,7 +2177,7 @@ describe('MDC-based MatMenu', () => {
compileTestComponent('rtl');
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '50%';
instance.rootTriggerEl.nativeElement.style.top = '50%';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2187,7 +2187,7 @@ describe('MDC-based MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-mdc-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.left)).toBe(Math.round(panelRect.right));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
Expand All @@ -2197,7 +2197,7 @@ describe('MDC-based MatMenu', () => {
compileTestComponent('rtl');
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '10px';
instance.rootTriggerEl.nativeElement.style.top = '50%';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2207,12 +2207,32 @@ describe('MDC-based MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-mdc-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.right)).toBe(Math.round(panelRect.left));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
}));

it('should account for custom padding when offsetting the sub-menu', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '10px';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();

// Change the padding to something different from the default.
(overlay.querySelector('.mat-mdc-menu-content') as HTMLElement).style.padding = '15px 0';

instance.levelOneTrigger.openMenu();
fixture.detectChanges();

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-mdc-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + 15);
});

it('should close all of the menus when an item is clicked', fakeAsync(() => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.click();
Expand Down
22 changes: 20 additions & 2 deletions src/material/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export const MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER = {
useFactory: MAT_MENU_SCROLL_STRATEGY_FACTORY,
};

/** Default top padding of the menu panel. */
/**
* Default top padding of the menu panel.
* @deprecated No longer being used. Will be removed.
* @breaking-change 15.0.0
*/
export const MENU_PANEL_TOP_PADDING = 8;

/** Options for binding a passive event listener. */
Expand Down Expand Up @@ -98,6 +102,12 @@ export abstract class _MatMenuTriggerBase implements AfterContentInit, OnDestroy
*/
private _parentMaterialMenu: _MatMenuBase | undefined;

/**
* Cached value of the padding of the parent menu panel.
* Used to offset sub-menus to compensate for the padding.
*/
private _parentInnerPadding: number | undefined;

/**
* Handles touch start events on the trigger.
* Needs to be an arrow function so we can easily use addEventListener and removeEventListener.
Expand Down Expand Up @@ -511,7 +521,15 @@ export abstract class _MatMenuTriggerBase implements AfterContentInit, OnDestroy
// to the edges of the trigger, instead of overlapping it.
overlayFallbackX = originX = menu.xPosition === 'before' ? 'start' : 'end';
originFallbackX = overlayX = originX === 'end' ? 'start' : 'end';
offsetY = overlayY === 'bottom' ? MENU_PANEL_TOP_PADDING : -MENU_PANEL_TOP_PADDING;

if (this._parentMaterialMenu) {
if (this._parentInnerPadding == null) {
const firstItem = this._parentMaterialMenu.items.first;
this._parentInnerPadding = firstItem ? firstItem._getHostElement().offsetTop : 0;
}

offsetY = overlayY === 'bottom' ? this._parentInnerPadding : -this._parentInnerPadding;
}
} else if (!menu.overlapTrigger) {
originY = overlayY === 'top' ? 'bottom' : 'top';
originFallbackY = overlayFallbackY === 'top' ? 'bottom' : 'top';
Expand Down
4 changes: 4 additions & 0 deletions src/material/menu/menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ mat-menu {
// collapse it to zero when they scroll away.
min-height: menu-common.$item-height + $vertical-padding * 2;

// Nothing in CSS depends upon this, but we need it so that the `offsetParent` in JS is the
// menu rather than the overlay panel. This comes into play when we read the `offsetTop`.
position: relative;

// Prevent users from interacting with the panel while it's animating. Note that
// people won't be able to click through it, because the overlay pane will catch the click.
// This fixes the following issues:
Expand Down
40 changes: 31 additions & 9 deletions src/material/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ import {
MenuPositionX,
MenuPositionY,
} from './index';
import {MAT_MENU_SCROLL_STRATEGY, MENU_PANEL_TOP_PADDING} from './menu-trigger';
import {MAT_MENU_SCROLL_STRATEGY} from './menu-trigger';

const MENU_PANEL_TOP_PADDING = 8;

describe('MatMenu', () => {
let overlayContainerElement: HTMLElement;
Expand Down Expand Up @@ -2128,7 +2130,7 @@ describe('MatMenu', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '50px';
instance.rootTriggerEl.nativeElement.style.top = '50px';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2138,7 +2140,7 @@ describe('MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.right)).toBe(Math.round(panelRect.left));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
Expand All @@ -2148,7 +2150,7 @@ describe('MatMenu', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.right = '10px';
instance.rootTriggerEl.nativeElement.style.top = '50%';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2158,7 +2160,7 @@ describe('MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.left)).toBe(Math.round(panelRect.right));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
Expand All @@ -2168,7 +2170,7 @@ describe('MatMenu', () => {
compileTestComponent('rtl');
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '50%';
instance.rootTriggerEl.nativeElement.style.top = '50%';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2178,7 +2180,7 @@ describe('MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.left)).toBe(Math.round(panelRect.right));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
Expand All @@ -2188,7 +2190,7 @@ describe('MatMenu', () => {
compileTestComponent('rtl');
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '10px';
instance.rootTriggerEl.nativeElement.style.top = '50%';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();
tick(500);
Expand All @@ -2198,12 +2200,32 @@ describe('MatMenu', () => {
tick(500);

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.right)).toBe(Math.round(panelRect.left));
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
}));

it('should account for custom padding when offsetting the sub-menu', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
instance.rootTriggerEl.nativeElement.style.left = '10px';
instance.rootTriggerEl.nativeElement.style.top = '200px';
instance.rootTrigger.openMenu();
fixture.detectChanges();

// Change the padding to something different from the default.
(overlay.querySelector('.mat-menu-content') as HTMLElement).style.padding = '15px 0';

instance.levelOneTrigger.openMenu();
fixture.detectChanges();

const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();

expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + 15);
});

it('should close all of the menus when an item is clicked', fakeAsync(() => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.click();
Expand Down
2 changes: 1 addition & 1 deletion tools/public_api_guard/material/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export abstract class _MatMenuTriggerBase implements AfterContentInit, OnDestroy
static ɵfac: i0.ɵɵFactoryDeclaration<_MatMenuTriggerBase, [null, null, null, null, { optional: true; }, { optional: true; self: true; }, { optional: true; }, null, null]>;
}

// @public
// @public @deprecated
const MENU_PANEL_TOP_PADDING = 8;

// @public
Expand Down

0 comments on commit 55c7726

Please sign in to comment.