Skip to content

Commit

Permalink
feat(menusurface): Add flipMenuHorizontally property, add unit tests.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 465404408
  • Loading branch information
joyzhong authored and copybara-github committed Aug 4, 2022
1 parent f8d950f commit 884c3a2
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 37 deletions.
8 changes: 4 additions & 4 deletions menu/lib/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {property, query} from 'lit/decorators';

import {List} from '../../list/lib/list';
import {ListItem} from '../../list/lib/listitem/list-item';
import {Corner, MenuCorner, MenuSurface} from '../../menusurface/lib/menu-surface';
import {Corner, MenuSurface} from '../../menusurface/lib/menu-surface';

import {MDCMenuAdapter} from './adapter';
import {DefaultFocusState as DefaultFocusStateEnum} from './constants';
Expand Down Expand Up @@ -54,7 +54,7 @@ export abstract class Menu extends LitElement {
// TODO(b/240174946): Add aria-label support.
// @property({type: String}) ariaLabel: string|null = null;

@property({type: String}) corner: Corner = 'TOP_START';
@property({type: String}) corner: Corner = 'BOTTOM_START';

@property({type: Number}) x: number|null = null;

Expand All @@ -70,7 +70,7 @@ export abstract class Menu extends LitElement {

@property({type: Boolean}) fullwidth = false;

@property({type: String}) menuCorner: MenuCorner = 'START';
@property({type: Boolean}) flipMenuHorizontally = false;

@property({type: Boolean}) stayOpenOnBodyClick: boolean = false;

Expand Down Expand Up @@ -126,7 +126,7 @@ export abstract class Menu extends LitElement {
.absolute=${this.absolute}
.fixed=${this.fixed}
.fullwidth=${this.fullwidth}
.menuCorner=${this.menuCorner}
.flipMenuHorizontally=${this.flipMenuHorizontally}
?stayOpenOnBodyClick=${this.stayOpenOnBodyClick}
class="md3-menu md3-menu-surface"
@closed=${this.onClosed}
Expand Down
8 changes: 4 additions & 4 deletions menusurface/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ const numbers = {
* Enum for bits in the {@see Corner) bitmap.
*/
enum CornerBit {
BOTTOM = 1,
CENTER = 2,
RIGHT = 4,
FLIP_RTL = 8,
BOTTOM = 1, // 0001
CENTER = 2, // 0010
RIGHT = 4, // 0100
FLIP_RTL = 8, // 1000
}

/**
Expand Down
6 changes: 3 additions & 3 deletions menusurface/lib/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class MDCMenuSurfaceFoundation {
}

/**
* Flip menu corner horizontally.
* Flips menu corner horizontally.
*/
flipCornerHorizontally() {
this.originCorner = this.originCorner ^ CornerBit.RIGHT;
Expand Down Expand Up @@ -313,7 +313,7 @@ export class MDCMenuSurfaceFoundation {
// Compute measurements for autoposition methods reuse.
this.measurements = this.getAutoLayoutmeasurements();

const corner = this.getoriginCorner();
const corner = this.getOriginCorner();
const maxMenuSurfaceHeight = this.getMenuSurfaceMaxHeight(corner);
const verticalAlignment =
this.hasBit(corner, CornerBit.BOTTOM) ? 'bottom' : 'top';
Expand Down Expand Up @@ -399,7 +399,7 @@ export class MDCMenuSurfaceFoundation {
* Only LEFT or RIGHT bit is used to position the menu surface ignoring RTL
* context. E.g., menu surface will be positioned from right side on TOP_END.
*/
private getoriginCorner(): Corner {
private getOriginCorner(): Corner {
let corner = this.originCorner;

const {viewportDistance, anchorSize, surfaceSize} = this.measurements;
Expand Down
42 changes: 16 additions & 26 deletions menusurface/lib/menu-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {MDCMenuSurfaceFoundation} from './foundation';

export type Corner = keyof typeof CornerEnum;
export type AnchorableElement = HTMLElement&{anchor: Element | null};
export type MenuCorner = 'START'|'END';

// tslint:disable:no-bitwise

Expand Down Expand Up @@ -120,43 +119,34 @@ export abstract class MenuSurface extends LitElement {
}
}
})

protected bitwiseCorner: CornerEnum = CornerEnum.TOP_START;
protected previousMenuCorner: MenuCorner|null = null;

// must be defined before observer of anchor corner for initialization
@property({type: String})
@observer(function(this: MenuSurface, value: MenuCorner) {
if (this.mdcFoundation) {
const isValidValue = value === 'START' || value === 'END';
const isFirstTimeSet = this.previousMenuCorner === null;
const cornerChanged =
!isFirstTimeSet && value !== this.previousMenuCorner;
const initiallySetToEnd = isFirstTimeSet && value === 'END';

if (isValidValue && (cornerChanged || initiallySetToEnd)) {
this.bitwiseCorner = this.bitwiseCorner ^ CornerBit.RIGHT;
this.mdcFoundation.flipCornerHorizontally();
this.previousMenuCorner = value;
}
protected previousFlipMenuHorizontally = false;

/**
* Whether to align the menu surface to the opposite side of the default
* alignment.
*/
@observer(function(this: MenuSurface, flipMenuHorizontally: boolean) {
if (!this.mdcFoundation) return;

if (this.previousFlipMenuHorizontally !== flipMenuHorizontally) {
this.mdcFoundation.flipCornerHorizontally();
}
this.previousFlipMenuHorizontally = flipMenuHorizontally;
})
menuCorner: MenuCorner = 'START';
@property({type: Boolean})
flipMenuHorizontally = false;

@property({type: String})
@observer(function(this: MenuSurface, value: Corner) {
if (this.mdcFoundation) {
if (value) {
let newCorner = stringToCorner[value];
if (this.menuCorner === 'END') {
newCorner = newCorner ^ CornerBit.RIGHT;
}

this.bitwiseCorner = newCorner;
this.bitwiseCorner = stringToCorner[value];
}
}
})
corner: Corner = 'TOP_START';
corner: Corner = 'BOTTOM_START';

@state() protected styleTop = '';
@state() protected styleLeft = '';
Expand Down
161 changes: 161 additions & 0 deletions menusurface/menu-surface_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import './menu-surface';
import '../list/list-item';

import {Environment} from '@material/web/testing/environment';
import {listenOnce} from '@material/web/testing/events';
import {html} from 'lit';
import {styleMap} from 'lit/directives/style-map';

import {MdMenuSurface} from './menu-surface';

const ANCHOR_WIDTH = '100px';
const ANCHOR_HEIGHT = '50px';

describe('menu surface tests', () => {
const env = new Environment();
let surface: MdMenuSurface;
let root: HTMLElement; // Inner root element to which styling is applied.

beforeEach(async () => {
const el = env.render(getMenuSurfaceTemplate());
surface = el.querySelector('md-menu-surface')!;
const button = el.querySelector('button')!;
surface.anchor = button;
await surface.updateComplete;
root = surface.shadowRoot!.querySelector('.md3-menu-surface')!;
});

describe('menu positioning options', () => {
it('Corner.TOP_START positions menu correctly', async () => {
surface.corner = 'TOP_START';
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe('0px');
expect(root.style.left).toBe('0px');
});

it('Corner.TOP_END positions menu correctly', async () => {
surface.corner = 'TOP_END';
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe('0px');
expect(root.style.left).toBe(ANCHOR_WIDTH);
});

it('Corner.BOTTOM_START positions menu correctly', async () => {
surface.corner = 'BOTTOM_START';
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe(ANCHOR_HEIGHT);
expect(root.style.left).toBe('0px');
});

it('Corner.BOTTOM_END positions menu correctly', async () => {
surface.corner = 'BOTTOM_END';
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe(ANCHOR_HEIGHT);
expect(root.style.left).toBe(ANCHOR_WIDTH);
});

it('`flipMenuHorizontally` flips TOP_START corner', async () => {
surface.corner = 'TOP_START';
surface.flipMenuHorizontally = true;
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe('0px');
expect(root.style.right).toBe('0px');
});

it('`flipMenuHorizontally` flips TOP_END corner', async () => {
surface.corner = 'TOP_END';
surface.flipMenuHorizontally = true;
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe('0px');
expect(root.style.right).toBe(ANCHOR_WIDTH);
});

it('`flipMenuHorizontally` flips BOTTOM_START corner', async () => {
surface.corner = 'BOTTOM_START';
surface.flipMenuHorizontally = true;
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe(ANCHOR_HEIGHT);
expect(root.style.right).toBe('0px');
});

it('`flipMenuHorizontally` flips BOTTOM_END corner', async () => {
surface.corner = 'BOTTOM_END';
surface.flipMenuHorizontally = true;
await surface.updateComplete;
surface.show();
// TOOD(b/241244423): Waiting for the event fired can be removed when
// this bug is fixed.
await listenOnce(surface, 'opened');
await surface.updateComplete;

expect(root.style.top).toBe(ANCHOR_HEIGHT);
expect(root.style.right).toBe(ANCHOR_WIDTH);
});
});
});

function getMenuSurfaceTemplate(propsInit: Partial<MdMenuSurface> = {}) {
return html`
<div class="root" style="position: relative; margin: 100px;">
<button style="${styleMap({
width: ANCHOR_WIDTH,
height: ANCHOR_HEIGHT
})}">
Open Menu
</button>
<md-menu-surface .quick="${propsInit.quick ?? true}"
.corner="${propsInit.corner ?? 'BOTTOM_START'}"
.flipMenuHorizontally="${propsInit.flipMenuHorizontally ?? false}">
Menu surface content
</md-menu-surface>
</div>
`;
}
21 changes: 21 additions & 0 deletions testing/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Adds an event listener for `eventName` on the given element.
* @return Promise that resolves when `eventName` has been fired on the element.
*/
export function listenOnce(
element: HTMLElement, eventName: string): Promise<CustomEvent> {
return new Promise((res) => {
const listener = (e: CustomEvent) => {
element.removeEventListener(eventName, listener as EventListener);
res(e);
};

element.addEventListener(eventName, listener as EventListener);
});
}

0 comments on commit 884c3a2

Please sign in to comment.