diff --git a/resources/mmv/ui/mmv.ui.metadataPanelScroller.js b/resources/mmv/ui/mmv.ui.metadataPanelScroller.js index ceaf85371..76129692d 100644 --- a/resources/mmv/ui/mmv.ui.metadataPanelScroller.js +++ b/resources/mmv/ui/mmv.ui.metadataPanelScroller.js @@ -39,11 +39,21 @@ */ this.hasAnimatedMetadata = undefined; + /** + * Timer used to highlight the chevron when the wrong key is pressed + * @property {number} + * @private + */ + this.highlightTimeout = undefined; + this.initialize(); } oo.inheritClass( MetadataPanelScroller, mw.mmv.ui.Element ); MPSP = MetadataPanelScroller.prototype; + MPSP.highlightDuration = 500; + MPSP.toggleScrollDuration = 400; + MPSP.attach = function() { var panel = this; @@ -69,6 +79,10 @@ // need to remove this to avoid animating again when reopening lightbox on same page this.$container.removeClass( 'invite' ); + + if ( this.highlightTimeout ) { + clearTimeout( this.highlightTimeout ); + } }; MPSP.initialize = function () { @@ -100,7 +114,8 @@ * Toggles the metadata div being totally visible. */ MPSP.toggle = function ( forceDirection ) { - var scrollTopWhenOpen = this.$container.outerHeight() - this.$controlBar.outerHeight(), + var self = this, + scrollTopWhenOpen = this.$container.outerHeight() - this.$controlBar.outerHeight(), scrollTopWhenClosed = 0, scrollTop = $.scrollTo().scrollTop(), panelIsOpen = scrollTop > scrollTopWhenClosed, @@ -110,14 +125,25 @@ scrollTopTarget = forceDirection === 'down' ? scrollTopWhenClosed : scrollTopWhenOpen; if ( scrollTop === scrollTopTarget ) { // The user pressed down when the panel was closed already (or up when fully open). - // Not a real toggle; do not log. + // Not a real toggle; highlight the chevron to attract attention. + this.$container.addClass( 'mw-mmv-highlight-chevron' ); + + if ( this.highlightTimeout ) { + clearTimeout( this.highlightTimeout ); + } + + this.highlightTimeout = setTimeout( function() { + if ( self.$container ) { + self.$container.removeClass( 'mw-mmv-highlight-chevron' ); + } + }, this.highlightDuration ); return; } } mw.mmv.logger.log( scrollTopTarget === scrollTopWhenOpen ? 'metadata-open' : 'metadata-close' ); - $.scrollTo( scrollTopTarget, 400 ); + $.scrollTo( scrollTopTarget, this.toggleScrollDuration ); }; /** @@ -132,7 +158,7 @@ targetHeight = $target.height(), targetTop = $target.offset().top, targetBottom = targetTop + targetHeight, - viewportHeight = $(window).height(), + viewportHeight = $( window ).height(), viewportTop = $.scrollTo().scrollTop(), viewportBottom = viewportTop + viewportHeight; diff --git a/resources/mmv/ui/mmv.ui.metadataPanelScroller.less b/resources/mmv/ui/mmv.ui.metadataPanelScroller.less index 1facb3a6d..0c290d0f9 100644 --- a/resources/mmv/ui/mmv.ui.metadataPanelScroller.less +++ b/resources/mmv/ui/mmv.ui.metadataPanelScroller.less @@ -86,7 +86,9 @@ background-position: center top; .rotate(180deg); } - .mw-mmv-post-image.invite & { + + .mw-mmv-post-image.invite &, .mw-mmv-post-image.mw-mmv-highlight-chevron & { + /* @embed */ background-image: url(img/drag-active.svg); opacity: 0.9; } diff --git a/tests/qunit/mmv/mmv.lightboxinterface.test.js b/tests/qunit/mmv/mmv.lightboxinterface.test.js index 0987c39d1..00e477ccf 100644 --- a/tests/qunit/mmv/mmv.lightboxinterface.test.js +++ b/tests/qunit/mmv/mmv.lightboxinterface.test.js @@ -240,183 +240,6 @@ restoreScrollTo(); } ); - /** - * We need to set up a proxy on the jQuery scrollTop function and the jQuery.scrollTo plugin, - * that will let us pretend that the document really scrolled and that will return values - * as if the scroll happened. - * @param {sinon.sandbox} sandbox - * @param {mw.mmv.LightboxInterface} ui - */ - function stubScrollFunctions( sandbox, ui ) { - var memorizedScrollToScroll = 0, - originalJQueryScrollTop = $.fn.scrollTop, - originalJQueryScrollTo = $.scrollTo; - - sandbox.stub( $.fn, 'scrollTop', function ( scrollTop ) { - // On some browsers $.scrollTo() != $document - if ( $.scrollTo().is( this ) ) { - if ( scrollTop !== undefined ) { - memorizedScrollToScroll = scrollTop; - return this; - } else { - return memorizedScrollToScroll; - } - } - - return originalJQueryScrollTop.call( this, scrollTop ); - } ); - - sandbox.stub( $, 'scrollTo', function ( scrollTo ) { - var $element; - - if ( scrollTo !== undefined ) { - memorizedScrollToScroll = scrollTo; - } - - $element = originalJQueryScrollTo.call( this, scrollTo, 0 ); - - if ( scrollTo !== undefined ) { - // Trigger event manually - ui.panel.scroller.scroll(); - } - - return $element; - } ); - } - - QUnit.test( 'Metadata scrolling', 14, function ( assert ) { - var ui = new mw.mmv.LightboxInterface(), - keydown = $.Event( 'keydown' ), - $document = $( document ); - - stubScrollFunctions( this.sandbox, ui ); - - // First phase of the test: up and down arrows - - ui.panel.scroller.hasAnimatedMetadata = false; - localStorage.removeItem( 'mmv.hasOpenedMetadata' ); - - // Attach lightbox to testing fixture to avoid interference with other tests. - ui.attach( '#qunit-fixture' ); - - assert.strictEqual( $.scrollTo().scrollTop(), 0, 'scrollTo scrollTop should be set to 0' ); - assert.ok( !ui.panel.scroller.$dragIcon.hasClass( 'pointing-down' ), - 'Chevron pointing up' ); - - assert.ok( !localStorage.getItem( 'mmv.hasOpenedMetadata' ), - 'The metadata hasn\'t been open yet, no entry in localStorage' ); - - keydown.which = 40; // Down arrow - $document.trigger( keydown ); - - keydown.which = 38; // Up arrow - $document.trigger( keydown ); - - assert.strictEqual( Math.round( $.scrollTo().scrollTop() ), - ui.panel.$imageMetadata.outerHeight(), - 'scrollTo scrollTop should be set to the metadata height after pressing up arrow' ); - assert.ok( ui.panel.scroller.$dragIcon.hasClass( 'pointing-down' ), - 'Chevron pointing down after pressing up arrow' ); - assert.ok( localStorage.getItem( 'mmv.hasOpenedMetadata' ), - 'localStorage knows that the metadata has been open' ); - - keydown.which = 40; // Down arrow - $document.trigger( keydown ); - - assert.strictEqual( $.scrollTo().scrollTop(), 0, - 'scrollTo scrollTop should be set to 0 after pressing down arrow' ); - assert.ok( !ui.panel.scroller.$dragIcon.hasClass( 'pointing-down' ), - 'Chevron pointing up after pressing down arrow' ); - - ui.panel.scroller.$dragIcon.click(); - - assert.strictEqual( Math.round( $.scrollTo().scrollTop() ), - ui.panel.$imageMetadata.outerHeight(), - 'scrollTo scrollTop should be set to the metadata height after clicking the chevron once' ); - assert.ok( ui.panel.scroller.$dragIcon.hasClass( 'pointing-down' ), - 'Chevron pointing down after clicking the chevron once' ); - - ui.panel.scroller.$dragIcon.click(); - - assert.strictEqual( $.scrollTo().scrollTop(), 0, - 'scrollTo scrollTop should be set to 0 after clicking the chevron twice' ); - assert.ok( !ui.panel.scroller.$dragIcon.hasClass( 'pointing-down' ), - 'Chevron pointing up after clicking the chevron twice' ); - - // Unattach lightbox from document - ui.unattach(); - - - // Second phase of the test: scroll memory - - // Attach lightbox to testing fixture to avoid interference with other tests. - ui.attach( '#qunit-fixture' ); - - // To make sure that the details are out of view, the lightbox is supposed to scroll to the top when open - assert.strictEqual( $.scrollTo().scrollTop(), 0, 'Page scrollTop should be set to 0' ); - - // Scroll down to check that the scrollTop memory doesn't affect prev/next (bug 59861) - $.scrollTo( 20, 0 ); - - // This extra attach() call simulates the effect of prev/next seen in bug 59861 - ui.attach( '#qunit-fixture' ); - - // The lightbox was already open at this point, the scrollTop should be left untouched - assert.strictEqual( $.scrollTo().scrollTop(), 20, 'Page scrollTop should be set to 20' ); - - // Unattach lightbox from document - ui.unattach(); - } ); - - QUnit.test( 'Metadata scroll logging', 6, function ( assert ) { - var ui = new mw.mmv.LightboxInterface(), - keydown = $.Event( 'keydown' ), - $document = $( document ); - - stubScrollFunctions( this.sandbox, ui ); - this.sandbox.stub( mw.mmv.logger, 'log' ); - - // Attach lightbox to testing fixture to avoid interference with other tests. - ui.attach( '#qunit-fixture' ); - - keydown.which = 40; // Down arrow - $document.trigger( keydown ); - - assert.ok( !mw.mmv.logger.log.called, 'Closing keypress not logged when the panel is closed already' ); - mw.mmv.logger.log.reset(); - - keydown.which = 38; // Up arrow - $document.trigger( keydown ); - - assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-open' ), 'Opening keypress logged' ); - mw.mmv.logger.log.reset(); - - keydown.which = 38; // Up arrow - $document.trigger( keydown ); - - assert.ok( !mw.mmv.logger.log.called, 'Opening keypress not logged when the panel is opened already' ); - mw.mmv.logger.log.reset(); - - keydown.which = 40; // Down arrow - $document.trigger( keydown ); - - assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-close' ), 'Closing keypress logged' ); - mw.mmv.logger.log.reset(); - - ui.panel.scroller.$dragIcon.click(); - - assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-open' ), 'Opening click logged' ); - mw.mmv.logger.log.reset(); - - ui.panel.scroller.$dragIcon.click(); - - assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-close' ), 'Closing click logged' ); - mw.mmv.logger.log.reset(); - - // Unattach lightbox from document - ui.unattach(); - } ); - QUnit.test( 'Keyboard prev/next', 2, function ( assert ) { var viewer = new mw.mmv.MultimediaViewer(), lightbox = new mw.mmv.LightboxInterface(); diff --git a/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js b/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js index 45a380f36..792521803 100644 --- a/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js +++ b/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js @@ -16,7 +16,11 @@ */ ( function( mw, $ ) { - QUnit.module( 'mmv.ui.metadataPanelScroller', QUnit.newMwEnvironment() ); + QUnit.module( 'mmv.ui.metadataPanelScroller', QUnit.newMwEnvironment( { + setup: function () { + this.clock = this.sandbox.useFakeTimers(); + } + } ) ); QUnit.test( 'empty()', 2, function ( assert ) { var $qf = $( '#qunit-fixture' ), @@ -84,4 +88,191 @@ assert.ok( scroller.savedHasOpenedMetadata, 'Full localStorage, we don\'t try to save the opened flag more than once' ); } ); + + /** + * We need to set up a proxy on the jQuery scrollTop function and the jQuery.scrollTo plugin, + * that will let us pretend that the document really scrolled and that will return values + * as if the scroll happened. + * @param {sinon.sandbox} sandbox + * @param {mw.mmv.ui.MetadataPanelScroller} scroller + */ + function stubScrollFunctions( sandbox, scroller ) { + var memorizedScrollToScroll = 0, + originalJQueryScrollTop = $.fn.scrollTop, + originalJQueryScrollTo = $.scrollTo; + + sandbox.stub( $.fn, 'scrollTop', function ( scrollTop ) { + // On some browsers $.scrollTo() != $document + if ( $.scrollTo().is( this ) ) { + if ( scrollTop !== undefined ) { + memorizedScrollToScroll = scrollTop; + return this; + } else { + return memorizedScrollToScroll; + } + } + + return originalJQueryScrollTop.call( this, scrollTop ); + } ); + + sandbox.stub( $, 'scrollTo', function ( scrollTo ) { + var $element; + + if ( scrollTo !== undefined ) { + memorizedScrollToScroll = scrollTo; + } + + $element = originalJQueryScrollTo.call( this, scrollTo, 0 ); + + if ( scrollTo !== undefined ) { + // Trigger event manually + scroller.scroll(); + } + + return $element; + } ); + } + + QUnit.test( 'Metadata scrolling', 12, function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $container = $( '
' ).css( 'height', 100 ).appendTo( $qf ), + $controlBar = $( '
' ).css( 'height', 50 ).appendTo( $container ), + fakeLocalStorage = { getItem : $.noop, setItem : $.noop }, + scroller = new mw.mmv.ui.MetadataPanelScroller( $container, $controlBar, fakeLocalStorage), + keydown = $.Event( 'keydown' ); + + stubScrollFunctions( this.sandbox, scroller ); + + this.sandbox.stub( fakeLocalStorage, 'setItem' ); + + // First phase of the test: up and down arrows + + scroller.hasAnimatedMetadata = false; + + scroller.attach(); + + assert.strictEqual( $.scrollTo().scrollTop(), 0, 'scrollTo scrollTop should be set to 0' ); + assert.ok( !scroller.$dragIcon.hasClass( 'pointing-down' ), + 'Chevron pointing up' ); + + assert.ok( !fakeLocalStorage.setItem.called, 'The metadata hasn\'t been open yet, no entry in localStorage' ); + + keydown.which = 40; // Down arrow + scroller.keydown( keydown ); + this.clock.tick( scroller.highlightDuration ); + + keydown.which = 38; // Up arrow + scroller.keydown( keydown ); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.ok( scroller.$dragIcon.hasClass( 'pointing-down' ), + 'Chevron pointing down after pressing up arrow' ); + assert.ok( fakeLocalStorage.setItem.calledWithExactly( 'mmv.hasOpenedMetadata', true ), 'localStorage knows that the metadata has been open' ); + + keydown.which = 40; // Down arrow + scroller.keydown( keydown ); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.strictEqual( $.scrollTo().scrollTop(), 0, + 'scrollTo scrollTop should be set to 0 after pressing down arrow' ); + assert.ok( !scroller.$dragIcon.hasClass( 'pointing-down' ), + 'Chevron pointing up after pressing down arrow' ); + + scroller.$dragIcon.click(); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.ok( scroller.$dragIcon.hasClass( 'pointing-down' ), + 'Chevron pointing down after clicking the chevron once' ); + + scroller.$dragIcon.click(); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.strictEqual( $.scrollTo().scrollTop(), 0, + 'scrollTo scrollTop should be set to 0 after clicking the chevron twice' ); + assert.ok( !scroller.$dragIcon.hasClass( 'pointing-down' ), + 'Chevron pointing up after clicking the chevron twice' ); + + // Unattach lightbox from document + scroller.unattach(); + + + // Second phase of the test: scroll memory + + scroller.attach(); + + // To make sure that the details are out of view, the lightbox is supposed to scroll to the top when open + assert.strictEqual( $.scrollTo().scrollTop(), 0, 'Page scrollTop should be set to 0' ); + + // Scroll down to check that the scrollTop memory doesn't affect prev/next (bug 59861) + $.scrollTo( 20, 0 ); + this.clock.tick( 100 ); + + // This extra attach() call simulates the effect of prev/next seen in bug 59861 + scroller.attach(); + + // The lightbox was already open at this point, the scrollTop should be left untouched + assert.strictEqual( $.scrollTo().scrollTop(), 20, 'Page scrollTop should be set to 20' ); + + scroller.unattach(); + } ); + + QUnit.test( 'Metadata scroll logging', 12, function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $container = $( '
' ).css( 'height', 100 ).appendTo( $qf ), + $controlBar = $( '
' ).css( 'height', 50 ).appendTo( $container ), + scroller = new mw.mmv.ui.MetadataPanelScroller( $container, $controlBar ), + keydown = $.Event( 'keydown' ); + + stubScrollFunctions( this.sandbox, scroller ); + + this.sandbox.stub( mw.mmv.logger, 'log' ); + + assert.ok( !$container.hasClass( 'mw-mmv-highlight-chevron' ), 'Chevron is not highlighted' ); + + keydown.which = 40; // Down arrow + scroller.keydown( keydown ); + + assert.ok( !mw.mmv.logger.log.called, 'Closing keypress not logged when the panel is closed already' ); + assert.ok( $container.hasClass( 'mw-mmv-highlight-chevron' ), 'Chevron is highlighted' ); + this.clock.tick( scroller.highlightDuration ); + assert.ok( !$container.hasClass( 'mw-mmv-highlight-chevron' ), 'Chevron is not highlighted' ); + mw.mmv.logger.log.reset(); + + keydown.which = 38; // Up arrow + scroller.keydown( keydown ); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-open' ), 'Opening keypress logged' ); + mw.mmv.logger.log.reset(); + + assert.ok( !$container.hasClass( 'mw-mmv-highlight-chevron' ), 'Chevron is not highlighted' ); + + keydown.which = 38; // Up arrow + scroller.keydown( keydown ); + + assert.ok( !mw.mmv.logger.log.called, 'Opening keypress not logged when the panel is opened already' ); + assert.ok( $container.hasClass( 'mw-mmv-highlight-chevron' ), 'Chevron is highlighted' ); + this.clock.tick( scroller.highlightDuration ); + assert.ok( !$container.hasClass( 'mw-mmv-highlight-chevron' ), 'Chevron is not highlighted' ); + mw.mmv.logger.log.reset(); + + keydown.which = 40; // Down arrow + scroller.keydown( keydown ); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-close' ), 'Closing keypress logged' ); + mw.mmv.logger.log.reset(); + + scroller.$dragIcon.click(); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-open' ), 'Opening click logged' ); + mw.mmv.logger.log.reset(); + + scroller.$dragIcon.click(); + this.clock.tick( scroller.toggleScrollDuration ); + + assert.ok( mw.mmv.logger.log.calledWithExactly( 'metadata-close' ), 'Closing click logged' ); + mw.mmv.logger.log.reset(); + } ); }( mediaWiki, jQuery ) );