Child elements also trigger previews

Follow up to
Iefe98c1f0422dbf034e385b1a41a859d030a2cf4 where we switched from the
jquery event delegation pattern to native methods. One thing that we
overlooked was that we also need to consider the case where the selector
matches the parent of an element, for example a span nested inside an
eligible link. I've rethought this logic, to first find the closest
eligible element to normalize the element passed to model methods
before running matchesSelector.

Bug: T325007
Change-Id: I4133751dc900a51829173e9c0d965cbb18e6a33e
This commit is contained in:
Jon Robson 2022-12-12 12:08:14 -08:00 committed by Jdlrobson
parent f642345625
commit 01e3ddcda5
5 changed files with 37 additions and 16 deletions

View file

@ -13,7 +13,7 @@
"include": [ "src/**/*.js" ],
"//": "Set the coverage percentage by category thresholds.",
"statements": 84,
"statements": 83,
"branches": 70,
"functions": 82,
"lines": 90,

Binary file not shown.

Binary file not shown.

View file

@ -25,8 +25,7 @@ import reducers from './reducers';
import createMediaWikiPopupsObject from './integrations/mwpopups';
import { previewTypes, getPreviewType,
registerModel,
isAnythingEligible,
isEligible } from './preview/model';
isAnythingEligible, findNearestEligibleTarget } from './preview/model';
import isReferencePreviewsEnabled from './isReferencePreviewsEnabled';
import setUserConfigFlags from './setUserConfigFlags';
import { registerGatewayForPreviewType, getGatewayForPreviewType } from './gateway';
@ -129,8 +128,8 @@ function registerChangeListeners(
*/
function handleDOMEventIfEligible( handler ) {
return function ( event ) {
const target = event.target;
if ( !isEligible( target ) ) {
const target = findNearestEligibleTarget( event.target );
if ( target === null ) {
return;
}
const mwTitle = titleFromElement( target, mw.config );

View file

@ -124,6 +124,39 @@ const elementMatchesSelector = ( element, selector ) => {
}
};
/**
* Emulates closest method for browsers that do not
* support it. e.g. IE11.
*
* @param {Element} element
* @param {string} selector
* @return {Element|null}
*/
function legacyClosest( element, selector ) {
const parentNode = element.parentNode;
if ( elementMatchesSelector( element, selector ) ) {
return element;
} else if ( !parentNode || parentNode === document.body ) {
// The `body` cannot be used as a preview selector.
return null;
}
return legacyClosest( parentNode, selector );
}
/**
* Recursively checks the element and its parents.
* @param {Element} element
* @return {Element|null}
*/
export function findNearestEligibleTarget( element ) {
const selector = selectors.join( ', ' );
if ( element.closest ) {
return element.closest( selector );
} else {
return legacyClosest( element, selector );
}
}
/**
* @typedef {Object} PreviewType
* @property {string} name identifier for preview type
@ -217,17 +250,6 @@ export function registerModel( type, selector ) {
} );
}
/**
* Check whether the element is eligible for previews.
*
* @param {Element} element
* @return {boolean}
*/
export function isEligible( element ) {
const validLinkSelector = selectors.join( ', ' );
return elementMatchesSelector( element, validLinkSelector );
}
/**
* Check whether any kind of preview is enabled.
*