mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Popups
synced 2024-11-23 23:24:39 +00:00
Show referencePreviews on click
Introducing the REFERENCE_CLICK action that will fetch and show the preview for the clicked reference right away without any delay. The main goal of the new chain of events introduced with the reference click is showing the reference preview right away. The actions triggered by the dwelling include delays in multiple parts of the process. If there's a dwell action-chain in progress when the click action is executed, the related promise ( that might still include steps with delays ) and the reference preview is retrieved and shown right away re-using the token. In the case where there's no dwell action running ( e.g. when the click was triggered via touch ) we create a new token and start from scratch. In either case we want to avoid, that multiple clicks trigger multiple actions and abort early when there's already a click action in progress. Bug: T218765 Change-Id: I073a93be2d17a21178aebe12267765f60a2811b9
This commit is contained in:
parent
e18e92dbf9
commit
1879a4d59e
|
@ -53,7 +53,7 @@
|
|||
"bundlesize": [
|
||||
{
|
||||
"path": "resources/dist/index.js",
|
||||
"maxSize": "13.5KB"
|
||||
"maxSize": "13.6KB"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
BIN
resources/dist/index.js
vendored
BIN
resources/dist/index.js
vendored
Binary file not shown.
BIN
resources/dist/index.js.map.json
vendored
BIN
resources/dist/index.js.map.json
vendored
Binary file not shown.
|
@ -8,6 +8,7 @@ export default {
|
|||
ABANDON_START: 'ABANDON_START',
|
||||
ABANDON_END: 'ABANDON_END',
|
||||
LINK_CLICK: 'LINK_CLICK',
|
||||
REFERENCE_CLICK: 'REFERENCE_CLICK',
|
||||
/** Precedes a fetch. */
|
||||
FETCH_START: 'FETCH_START',
|
||||
/** Follows a successful fetch. */
|
||||
|
|
|
@ -308,6 +308,59 @@ export function linkClick( el ) {
|
|||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the user clicking on a reference preview link with their mouse, keyboard, or an
|
||||
* assistive device.
|
||||
*
|
||||
* @param {mw.Title} title
|
||||
* @param {Element} el
|
||||
* @param {Gateway} gateway
|
||||
* @param {Function} generateToken
|
||||
* @return {Redux.Thunk}
|
||||
*/
|
||||
export function referenceClick( title, el, gateway, generateToken ) {
|
||||
return ( dispatch, getState ) => {
|
||||
const {
|
||||
activeLink,
|
||||
activeToken: dwellToken,
|
||||
promise: dwellPromise,
|
||||
wasClicked
|
||||
} = getState().preview;
|
||||
|
||||
if ( wasClicked ) {
|
||||
return $.Deferred().resolve().promise();
|
||||
}
|
||||
|
||||
const xhr = gateway.fetchPreviewForTitle( title, el );
|
||||
|
||||
function clickFollowsDwellEvent() {
|
||||
return activeLink === el && dwellToken !== '';
|
||||
}
|
||||
|
||||
let token = dwellToken;
|
||||
if ( !clickFollowsDwellEvent() ) {
|
||||
token = generateToken();
|
||||
} else {
|
||||
dwellPromise.abort();
|
||||
}
|
||||
|
||||
dispatch( timedAction( {
|
||||
type: types.REFERENCE_CLICK,
|
||||
el,
|
||||
token
|
||||
} ) );
|
||||
|
||||
return xhr.then( ( result ) => {
|
||||
dispatch( {
|
||||
type: types.FETCH_COMPLETE,
|
||||
el,
|
||||
result,
|
||||
token
|
||||
} );
|
||||
} );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the user dwelling on a preview with their mouse.
|
||||
*
|
||||
|
|
20
src/index.js
20
src/index.js
|
@ -263,11 +263,25 @@ function registerChangeListeners(
|
|||
boundActions.abandon();
|
||||
}
|
||||
} )
|
||||
.on( 'click', validLinkSelector, function () {
|
||||
.on( 'click', validLinkSelector, function ( event ) {
|
||||
const mwTitle = titleFromElement( this, mw.config );
|
||||
|
||||
if ( mwTitle ) {
|
||||
boundActions.linkClick( this );
|
||||
const type = getPreviewType( this, mw.config, mwTitle );
|
||||
|
||||
switch ( type ) {
|
||||
case previewTypes.TYPE_PAGE:
|
||||
boundActions.linkClick( this );
|
||||
break;
|
||||
case previewTypes.TYPE_REFERENCE:
|
||||
event.preventDefault();
|
||||
boundActions.referenceClick(
|
||||
mwTitle,
|
||||
this,
|
||||
referenceGateway,
|
||||
generateToken
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} );
|
||||
}() );
|
||||
|
|
|
@ -17,7 +17,8 @@ export default function preview( state, action ) {
|
|||
activeEvent: undefined,
|
||||
activeToken: '',
|
||||
shouldShow: false,
|
||||
isUserDwelling: false
|
||||
isUserDwelling: false,
|
||||
wasClicked: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -56,6 +57,14 @@ export default function preview( state, action ) {
|
|||
isUserDwelling: true
|
||||
} );
|
||||
|
||||
case actionTypes.REFERENCE_CLICK:
|
||||
return nextState( state, {
|
||||
activeLink: action.el,
|
||||
activeToken: action.token,
|
||||
isUserDwelling: true,
|
||||
wasClicked: true
|
||||
} );
|
||||
|
||||
case actionTypes.ABANDON_END:
|
||||
if ( action.token === state.activeToken && !state.isUserDwelling ) {
|
||||
return nextState( state, {
|
||||
|
@ -75,7 +84,8 @@ export default function preview( state, action ) {
|
|||
|
||||
case actionTypes.ABANDON_START:
|
||||
return nextState( state, {
|
||||
isUserDwelling: false
|
||||
isUserDwelling: false,
|
||||
wasClicked: false
|
||||
} );
|
||||
|
||||
case actionTypes.FETCH_START:
|
||||
|
|
|
@ -613,3 +613,116 @@ QUnit.test( 'PREVIEW_SEEN action not called if preview type not page', function
|
|||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.module( 'ext.popups/actions#referenceClick @integration', {
|
||||
beforeEach() {
|
||||
this.state = {
|
||||
preview: {}
|
||||
};
|
||||
this.getState = () => this.state;
|
||||
|
||||
this.gatewayDeferred = $.Deferred();
|
||||
this.gateway = {
|
||||
fetchPreviewForTitle: this.sandbox.stub().returns(
|
||||
this.gatewayDeferred.resolve( {} ).promise()
|
||||
)
|
||||
};
|
||||
|
||||
// lets just set this to always return 0
|
||||
mw.now = () => 0;
|
||||
|
||||
setupEl( this );
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( '#referenceClick', function ( assert ) {
|
||||
const dispatch = this.sandbox.spy();
|
||||
|
||||
this.sandbox.stub( mw, 'now' ).returns( new Date() );
|
||||
|
||||
const referenceClicked = actions.referenceClick(
|
||||
this.title, this.el, this.gateway, generateToken
|
||||
)(
|
||||
dispatch,
|
||||
this.getState
|
||||
);
|
||||
|
||||
assert.propEqual(
|
||||
dispatch.getCall( 0 ).args[ 0 ], {
|
||||
type: actionTypes.REFERENCE_CLICK,
|
||||
el: this.el,
|
||||
token: 'ABC',
|
||||
timestamp: mw.now()
|
||||
},
|
||||
'The dispatcher was called with the correct REFERENCE_CLICK arguments.'
|
||||
);
|
||||
|
||||
return referenceClicked.then( () => {
|
||||
assert.propEqual(
|
||||
dispatch.getCall( 1 ).args[ 0 ], {
|
||||
type: actionTypes.FETCH_COMPLETE,
|
||||
el: this.el,
|
||||
result: {},
|
||||
token: 'ABC'
|
||||
},
|
||||
'The dispatcher was called with the correct FETCH_COMPLETE arguments.'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( '#referenceClick doesn\'t continue when clicked several times', function ( assert ) {
|
||||
const dispatch = this.sandbox.spy();
|
||||
|
||||
this.state.preview = {
|
||||
wasClicked: true
|
||||
};
|
||||
|
||||
const referenceClicked = actions.referenceClick(
|
||||
this.title, this.el, this.gateway, generateToken
|
||||
)(
|
||||
dispatch,
|
||||
this.getState
|
||||
);
|
||||
|
||||
return referenceClicked.then( () => {
|
||||
assert.ok(
|
||||
dispatch.notCalled,
|
||||
'The dispatcher was never called.'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( '#referenceClick re-uses the linkDwell token if present', function ( assert ) {
|
||||
const dispatch = this.sandbox.spy(),
|
||||
oldDeferred = $.Deferred().promise( { abort() {} } );
|
||||
|
||||
this.state.preview = {
|
||||
activeLink: this.el,
|
||||
activeToken: 'OLD_TOKEN',
|
||||
promise: oldDeferred
|
||||
};
|
||||
|
||||
const referenceClicked = actions.referenceClick(
|
||||
this.title, this.el, this.gateway, generateToken
|
||||
)(
|
||||
dispatch,
|
||||
this.getState
|
||||
);
|
||||
|
||||
assert.propEqual(
|
||||
dispatch.getCall( 0 ).args[ 0 ], {
|
||||
type: actionTypes.REFERENCE_CLICK,
|
||||
el: this.el,
|
||||
token: 'OLD_TOKEN',
|
||||
timestamp: mw.now()
|
||||
},
|
||||
'The dispatcher was called with the correct REFERENCE_CLICK arguments.'
|
||||
);
|
||||
|
||||
return referenceClicked.then( () => {
|
||||
assert.ok(
|
||||
dispatch.calledTwice,
|
||||
'The dispatcher was called twice.'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -21,7 +21,8 @@ QUnit.test( '@@INIT', ( assert ) => {
|
|||
activeEvent: undefined,
|
||||
activeToken: '',
|
||||
shouldShow: false,
|
||||
isUserDwelling: false
|
||||
isUserDwelling: false,
|
||||
wasClicked: false
|
||||
},
|
||||
'The initial state is correct.'
|
||||
);
|
||||
|
@ -196,6 +197,7 @@ QUnit.test( 'FETCH_COMPLETE', ( assert ) => {
|
|||
{
|
||||
activeToken: token,
|
||||
isUserDwelling: false, // Set when ABANDON_START is reduced.
|
||||
wasClicked: false,
|
||||
|
||||
fetchResponse: action.result,
|
||||
shouldShow: false
|
||||
|
@ -286,8 +288,28 @@ QUnit.test( 'ABANDON_START', ( assert ) => {
|
|||
assert.deepEqual(
|
||||
preview( {}, action ),
|
||||
{
|
||||
isUserDwelling: false
|
||||
isUserDwelling: false,
|
||||
wasClicked: false
|
||||
},
|
||||
'ABANDON_START should mark the preview having been abandoned.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'REFERENCE_CLICK updates the state for a click', function ( assert ) {
|
||||
const action = {
|
||||
type: actionTypes.REFERENCE_CLICK,
|
||||
el: this.el,
|
||||
token: '1234567890'
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
preview( {}, action ),
|
||||
{
|
||||
activeLink: action.el,
|
||||
activeToken: action.token,
|
||||
isUserDwelling: true,
|
||||
wasClicked: true
|
||||
},
|
||||
'It should set active link and token as well as dwelling and click status.'
|
||||
);
|
||||
} );
|
||||
|
|
|
@ -110,8 +110,8 @@ module.exports = ( env, argv ) => ( {
|
|||
// Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers
|
||||
// up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are
|
||||
// well understood. Related to bundlesize minified, gzipped compressed file size tests.
|
||||
maxAssetSize: 41 * 1024,
|
||||
maxEntrypointSize: 41 * 1024,
|
||||
maxAssetSize: 41.5 * 1024,
|
||||
maxEntrypointSize: 41.5 * 1024,
|
||||
|
||||
// The default filter excludes map files but we rename ours.
|
||||
assetFilter: ( filename ) => !filename.endsWith( srcMapExt )
|
||||
|
|
Loading…
Reference in a new issue