Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
feat(menu): Move current time retrieval to adapter.
Browse files Browse the repository at this point in the history
BREAKING CHANGE: adapters have to implement the new `getAccurateTime`
method.
  • Loading branch information
Sérgio Gomes authored and sgomes committed Feb 22, 2017
1 parent e0298d9 commit 4d0d587
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 67 deletions.
1 change: 1 addition & 0 deletions packages/mdc-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ The adapter for temporary drawers must provide the following functions, with cor
| `isRtl() => boolean` | Returns boolean indicating whether the current environment is RTL. |
| `setTransformOrigin(value: string) => void` | Sets the transform origin for the menu element. |
| `setPosition(position: { top: string, right: string, bottom: string, left: string }) => void` | Sets the position of the menu element. |
| `getAccurateTime() => number` | Returns a number representing the number of milliseconds (and fractional milliseconds, as the decimal part) since a given point in time, which should remain constant during the component's lifecycle. Ideally, this should be provided by `window.performance.now()`, which has enough precision for high frequency animation calculations. Using `Date.now()` or equivalent may result in some aliasing in animations. |

### The full foundation API

Expand Down
5 changes: 3 additions & 2 deletions packages/mdc-menu/simple/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {
isRtl: () => /* boolean */ false,
setTransformOrigin: (/* origin: string */) => {},
setPosition: (/* position: { top: string, right: string, bottom: string, left: string } */) => {},
getAccurateTime: () => /* number */ 0,
};
}

Expand Down Expand Up @@ -147,7 +148,7 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {

// Animate menu opening or closing.
animationLoop_() {
const time = window.performance.now();
const time = this.adapter_.getAccurateTime();
const {TRANSITION_DURATION_MS, TRANSITION_X1, TRANSITION_Y1, TRANSITION_X2, TRANSITION_Y2,
TRANSITION_SCALE_ADJUSTMENT_X, TRANSITION_SCALE_ADJUSTMENT_Y} = MDCSimpleMenuFoundation.numbers;
const currentTime = clamp((time - this.startTime_) / TRANSITION_DURATION_MS);
Expand Down Expand Up @@ -199,7 +200,7 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {

// Starts the open or close animation.
animateMenu_() {
this.startTime_ = window.performance.now();
this.startTime_ = this.adapter_.getAccurateTime();
this.startScaleX_ = this.scaleX_;
this.startScaleY_ = this.scaleY_;

Expand Down
1 change: 1 addition & 0 deletions packages/mdc-menu/simple/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class MDCSimpleMenu extends MDCComponent {
this.root_.style.top = 'top' in position ? position.top : null;
this.root_.style.bottom = 'bottom' in position ? position.bottom : null;
},
getAccurateTime: () => window.performance.now(),
});
}
}
83 changes: 18 additions & 65 deletions test/unit/mdc-menu/simple.foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ test('defaultAdapter returns a complete adapter implementation', () => {
'registerInteractionHandler', 'deregisterInteractionHandler', 'registerDocumentClickHandler',
'deregisterDocumentClickHandler', 'getYParamsForItemAtIndex', 'setTransitionDelayForItemAtIndex',
'getIndexForEventTarget', 'notifySelected', 'notifyCancel', 'saveFocus', 'restoreFocus', 'isFocused', 'focus',
'getFocusedItemIndex', 'focusItemAtIndex', 'isRtl', 'setTransformOrigin', 'setPosition',
'getFocusedItemIndex', 'focusItemAtIndex', 'isRtl', 'setTransformOrigin', 'setPosition', 'getAccurateTime',
]);
});

Expand Down Expand Up @@ -106,176 +106,135 @@ testFoundation('#open adds the open class to the menu', ({foundation, mockAdapte

testFoundation('#open removes the animation class at the end of the animation',
({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);
td.when(mockAdapter.hasClass('mdc-simple-menu--open-from-top-right')).thenReturn(true);

foundation.open();
mockRaf.flush();
mockRaf.flush();
td.verify(mockAdapter.addClass('mdc-simple-menu--animating'));

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
mockRaf.flush();
td.verify(mockAdapter.removeClass('mdc-simple-menu--animating'));

window.performance.now = now;
});

testFoundation('#open focuses the menu at the end of the animation', ({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);

foundation.open();
mockRaf.flush();
mockRaf.flush();

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
td.verify(mockAdapter.focus());

window.performance.now = now;
});

testFoundation('#open anchors the menu on the top left in LTR, given enough room',
({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockAdapter.hasAnchor()).thenReturn(true);
td.when(mockAdapter.isRtl()).thenReturn(false);
td.when(mockAdapter.getInnerDimensions()).thenReturn({height: 200, width: 100});
td.when(mockAdapter.getWindowDimensions()).thenReturn({height: 1000, width: 1000});
td.when(mockAdapter.getAnchorDimensions()).thenReturn({
height: 20, width: 40, top: 20, bottom: 40, left: 20, right: 60,
});
td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);

foundation.open();
mockRaf.flush();
mockRaf.flush();

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
td.verify(mockAdapter.setTransformOrigin('top left'));
td.verify(mockAdapter.setPosition({left: '0', top: '0'}));

window.performance.now = now;
});

testFoundation('#open anchors the menu on the top right in LTR when close to the right edge',
({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockAdapter.hasAnchor()).thenReturn(true);
td.when(mockAdapter.isRtl()).thenReturn(false);
td.when(mockAdapter.getInnerDimensions()).thenReturn({height: 200, width: 100});
td.when(mockAdapter.getWindowDimensions()).thenReturn({height: 1000, width: 1000});
td.when(mockAdapter.getAnchorDimensions()).thenReturn({
height: 20, width: 40, top: 20, bottom: 40, left: 950, right: 990,
});
td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);

foundation.open();
mockRaf.flush();
mockRaf.flush();

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
td.verify(mockAdapter.setTransformOrigin('top right'));
td.verify(mockAdapter.setPosition({right: '0', top: '0'}));

window.performance.now = now;
});

testFoundation('#open anchors the menu on the top right in RTL, given enough room',
({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockAdapter.hasAnchor()).thenReturn(true);
td.when(mockAdapter.isRtl()).thenReturn(true);
td.when(mockAdapter.getInnerDimensions()).thenReturn({height: 200, width: 100});
td.when(mockAdapter.getWindowDimensions()).thenReturn({height: 1000, width: 1000});
td.when(mockAdapter.getAnchorDimensions()).thenReturn({
height: 20, width: 40, top: 20, bottom: 40, left: 500, right: 540,
});
td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);

foundation.open();
mockRaf.flush();
mockRaf.flush();

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
td.verify(mockAdapter.setTransformOrigin('top right'));
td.verify(mockAdapter.setPosition({right: '0', top: '0'}));

window.performance.now = now;
});

testFoundation('#open anchors the menu on the top left in RTL when close to the left edge',
({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockAdapter.hasAnchor()).thenReturn(true);
td.when(mockAdapter.isRtl()).thenReturn(true);
td.when(mockAdapter.getInnerDimensions()).thenReturn({height: 200, width: 100});
td.when(mockAdapter.getWindowDimensions()).thenReturn({height: 1000, width: 1000});
td.when(mockAdapter.getAnchorDimensions()).thenReturn({
height: 20, width: 40, top: 20, bottom: 40, left: 10, right: 50,
});
td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);

foundation.open();
mockRaf.flush();
mockRaf.flush();

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
td.verify(mockAdapter.setTransformOrigin('top left'));
td.verify(mockAdapter.setPosition({left: '0', top: '0'}));

window.performance.now = now;
});

testFoundation('#open anchors the menu on the bottom left in LTR when close to the bottom edge',
({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockAdapter.hasAnchor()).thenReturn(true);
td.when(mockAdapter.isRtl()).thenReturn(false);
td.when(mockAdapter.getInnerDimensions()).thenReturn({height: 200, width: 100});
td.when(mockAdapter.getWindowDimensions()).thenReturn({height: 1000, width: 1000});
td.when(mockAdapter.getAnchorDimensions()).thenReturn({
height: 20, width: 40, top: 900, bottom: 920, left: 10, right: 50,
});
td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);

foundation.open();
mockRaf.flush();
mockRaf.flush();

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
td.verify(mockAdapter.setTransformOrigin('bottom left'));
td.verify(mockAdapter.setPosition({left: '0', bottom: '0'}));

window.performance.now = now;
});

testFoundation('#close adds the animation class to start an animation', ({foundation, mockAdapter, mockRaf}) => {
Expand All @@ -297,11 +256,7 @@ testFoundation('#close removes the open class from the menu', ({foundation, mock

testFoundation('#close removes the animation class at the end of the animation',
({foundation, mockAdapter, mockRaf}) => {
const {now} = window.performance;
const mockNow = td.func('window.performance.now');
window.performance.now = mockNow;

td.when(mockNow()).thenReturn(0);
td.when(mockAdapter.getAccurateTime()).thenReturn(0);
td.when(mockAdapter.hasClass('mdc-simple-menu--open')).thenReturn(true);
td.when(mockAdapter.hasClass('mdc-simple-menu--open-from-bottom-right')).thenReturn(true);

Expand All @@ -310,11 +265,9 @@ testFoundation('#close removes the animation class at the end of the animation',
mockRaf.flush();
td.verify(mockAdapter.addClass('mdc-simple-menu--animating'));

td.when(mockNow()).thenReturn(500);
td.when(mockAdapter.getAccurateTime()).thenReturn(500);
mockRaf.flush();
td.verify(mockAdapter.removeClass('mdc-simple-menu--animating'));

window.performance.now = now;
});

test('#isOpen returns true when the menu is open', () => {
Expand Down
12 changes: 12 additions & 0 deletions test/unit/mdc-menu/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,15 @@ test('bezierProgress returns the right values for a (1, 0, 0, 1) curve', () => {
};
testBezier(curve, expected);
});

test('bezierProgress returns the right values for a (0.99, 0.36, 0, 0.75) curve', () => {
const curve = {x1: 0.99, y1: 0.36, x2: 0, y2: 0.75};
const expected = {
0: 0,
0.2: 0.086,
0.5: 0.634,
0.8: 0.939,
1: 1,
};
testBezier(curve, expected);
});

0 comments on commit 4d0d587

Please sign in to comment.