Skip to content

Commit

Permalink
Support computers with touch screen (#215)
Browse files Browse the repository at this point in the history
* Popup position based on width

* Fix tests

* Split isTouch and isMobile

* Use isMobile for layout purposes

* fix merge

* Only register one event type

* distinguish tap from click

* Use mouseenter on mobile too

* Cleanup

* Tap on links should not navigate

* Use click and pointerenter instead of mouseenter

* Use pointerup instead of click

* Actively prevent navigation on preview links

* CI: use node@21

* Fix cypress tests

---------

Co-authored-by: Sbisson <[email protected]>
  • Loading branch information
stephanebisson and Sbisson authored Jul 25, 2024
1 parent da1f0ab commit 29bc25b
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 17, 'latest' ]
node: [ 21 ]

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules/
.DS_Store
storybook-static
coverage/
cypress/screenshots
5 changes: 4 additions & 1 deletion cypress/e2e/active.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ describe( 'Check the Hover and Click Event', () => {

it( 'Check the Preview by hovering over the Span and then leaving', () => {

preview.getPreviewSpan().first().trigger( 'mouseenter', 'right' )
preview.getPreviewSpan().first().trigger( 'pointerenter', {
position: 'right',
pointerType: 'mouse'
} )

preview.checkPreview()

Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/mobile-gallery.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe( 'Check the Gallery Pages in Mobile View', () => {

it( 'Check the Gallery Pages Movement by Swiping', () => {
// Open the Preview
preview.getPreviewSpan().first().click()
preview.getPreviewSpan().first().trigger( 'pointerenter', { pointerType: 'touch' } )
const i = 0
// Check if the Images exist
preview.getBodyGalleryImages().its( 'length' ).then( () => {
Expand Down
8 changes: 4 additions & 4 deletions cypress/e2e/mobile-layout.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe( 'Check the different Layout in Mobile Wikipedia Preview ', () => {

it( 'Check the Wikipedia Preview in Standard Layout', () => {
// Opens Mobile Preview
preview.getPreviewSpan().eq( 0 ).click()
preview.getPreviewSpan().eq( 0 ).trigger( 'pointerenter', { pointerType: 'touch' } )
// Checks the preview
preview.checkMobilePreview()
preview.checkPreview()
Expand All @@ -23,7 +23,7 @@ describe( 'Check the different Layout in Mobile Wikipedia Preview ', () => {
it.skip( 'Check the Wikipedia Preview in Offline Layout', () => {
goOffline()
// Opens Mobile Preview
preview.getPreviewSpan().first().click()
preview.getPreviewSpan().first().trigger( 'pointerenter', { pointerType: 'touch' } )
// Checks the preview
preview.checkMobilePreview()
preview.checkPreviewOffline()
Expand All @@ -35,7 +35,7 @@ describe( 'Check the different Layout in Mobile Wikipedia Preview ', () => {

it( 'Check the Wikipedia Preview in Error Layout', () => {
// Opens Mobile Preview
preview.getPreviewSpan().eq( 1 ).click()
preview.getPreviewSpan().eq( 1 ).trigger( 'pointerenter', { pointerType: 'touch' } )
// Checks the preview
preview.checkMobilePreview()
preview.checkPreviewError()
Expand All @@ -46,7 +46,7 @@ describe( 'Check the different Layout in Mobile Wikipedia Preview ', () => {

it( 'Check the Wikipedia Preview in Disambiguation Layout', () => {
// Opens Mobile Preview
preview.getPreviewSpan().eq( 2 ).click()
preview.getPreviewSpan().eq( 2 ).trigger( 'pointerenter', { pointerType: 'touch' } )
// Checks the preview
preview.checkMobilePreview()
preview.checkPreview()
Expand Down
8 changes: 4 additions & 4 deletions src/event.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isTouch, isVerticallyScrollable } from './utils'
import { isMobile, isVerticallyScrollable } from './utils'
import { showFullscreenGallery } from './gallery/fullscreen'

export const customEvents = ( popup ) => {
Expand Down Expand Up @@ -84,13 +84,13 @@ export const customEvents = ( popup ) => {
} )
}

if ( isTouch ) {
if ( element.component.closeBtn ) {
addEventListener( element.component.closeBtn, 'click', popup.hide )
}

if ( isTouch ) {
if ( isMobile ) {
const darkScreen = document.querySelector( '.wp-dark-screen' )
addEventListener( darkScreen, 'click', popup.hide, true )
addEventListener( darkScreen, 'pointerup', popup.hide, true )
} else {
addEventListener( element, 'mouseleave', onMouseLeave )
addEventListener( element.currentTargetElement, 'mouseleave', onMouseLeave )
Expand Down
64 changes: 40 additions & 24 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { createPopup } from './popup'
import { createTouchPopup } from './touchPopup'
import { renderPreview, renderLoading, renderError, renderDisambiguation, renderOffline } from './preview'
import {
getWikipediaAttrFromUrl, buildWikipediaUrl, isTouch, getDir, isOnline,
version, getAnalyticsQueryParam, getElement
getWikipediaAttrFromUrl, buildWikipediaUrl, getDir, isOnline,
version, getAnalyticsQueryParam, getElement, isMobile
} from './utils'

const invokeCallback = ( events, name, params ) => {
Expand All @@ -23,7 +23,7 @@ const invokeCallback = ( events, name, params ) => {
// getPreviewHtml is meant to be used by the Wordpress plugin only
const getPreviewHtml = ( title, lang, callback ) => {
requestPagePreviewWithMedia( lang, title, ( data ) => {
callback( renderPreview( lang, data, isTouch ) )
callback( renderPreview( lang, data, isMobile ) )
} )
}

Expand Down Expand Up @@ -74,7 +74,7 @@ function init( {
} ) {
popupContainer = getElement( popupContainer ) || document.body
const globalLang = lang
const popup = isTouch ?
const popup = isMobile ?
createTouchPopup( popupContainer ) :
createPopup( popupContainer )
const popupEvents = customEvents( popup )
Expand All @@ -85,6 +85,7 @@ function init( {

const showPopup = ( e, refresh = false ) => {
e.preventDefault()
e.stopPropagation()

const popupId = Date.now()
const { currentTarget } = refresh ? last : e
Expand All @@ -107,7 +108,7 @@ function init( {
popup.loading = true
popup.dir = dir
popup.show(
renderLoading( isTouch, localLang, dir, currentColorScheme ),
renderLoading( isMobile, localLang, dir, currentColorScheme ),
currentTarget,
pointerPosition
)
Expand All @@ -123,18 +124,18 @@ function init( {
popup.title = title
if ( data.type === 'standard' ) {
popup.show(
renderPreview( localLang, data, isTouch, currentColorScheme ),
renderPreview( localLang, data, isMobile, currentColorScheme ),
currentTarget,
pointerPosition
)
popup.media = data.media
invokeCallback( events, 'onShow', [ title, localLang, 'standard' ] )
} else if ( data.type === 'disambiguation' ) {
const content = data.extractHtml ?
renderPreview( localLang, data, isTouch, currentColorScheme ) :
renderPreview( localLang, data, isMobile, currentColorScheme ) :
// fallback message when no extract is found on disambiguation page
renderDisambiguation(
isTouch,
isMobile,
localLang,
data.title,
data.dir,
Expand All @@ -150,14 +151,14 @@ function init( {
} else {
if ( isOnline() ) {
popup.show(
renderError( isTouch, localLang, title, dir, currentColorScheme ),
renderError( isMobile, localLang, title, dir, currentColorScheme ),
currentTarget,
pointerPosition
)
invokeCallback( events, 'onShow', [ title, localLang, 'error' ] )
} else {
popup.show(
renderOffline( isTouch, localLang, dir, currentColorScheme ),
renderOffline( isMobile, localLang, dir, currentColorScheme ),
currentTarget,
pointerPosition
)
Expand Down Expand Up @@ -189,18 +190,34 @@ function init( {
} )
}

popup.subscribe( popupEvents )
const onPointerEnter = ( pointerEvent ) => {
showPopup( pointerEvent )
}

const registerPreviewEvents = ( node ) => {
node.addEventListener( 'pointerenter', onPointerEnter )
}

const preventTapFromNavigatingLink = ( node ) => {
// The click event still receives a MouseEvent instead of the newer PointerEvent
// in some browsers so we have to grab the pointerType from the preceding pointerdown event.
let currentPointerType = null
node.addEventListener( 'pointerdown', ( e ) => {
currentPointerType = e.pointerType
} )
node.addEventListener( 'click', ( e ) => {
if ( currentPointerType === 'touch' ) {
e.preventDefault()
e.stopPropagation()
}
} )
}

forEachRoot( root, ( localRoot ) => {
Array.prototype.forEach.call(
localRoot.querySelectorAll( selector ),
( node ) => {
if ( isTouch ) {
node.addEventListener( 'click', showPopup )
} else {
node.addEventListener( 'mouseenter', showPopup )
}

registerPreviewEvents( node )
foundSelectorLinks.push( {
text: node.textContent,
title: node.getAttribute( 'data-wp-title' ) || node.textContent,
Expand All @@ -219,11 +236,8 @@ function init( {
if ( matches ) {
node.setAttribute( 'data-wp-title', matches.title )
node.setAttribute( 'data-wp-lang', matches.lang )
if ( isTouch ) {
node.addEventListener( 'click', showPopup )
} else {
node.addEventListener( 'mouseenter', showPopup )
}
registerPreviewEvents( node )
preventTapFromNavigatingLink( node )

foundDetectLinks.push( {
text: node.textContent,
Expand All @@ -236,18 +250,20 @@ function init( {
} )
}

popup.subscribe( popupEvents )

if ( debug ) {
/* eslint-disable no-console */
console.group( 'Wikipedia Preview [debug mode]' )
console.group( `Searching for "${ selector }" inside ${ root }, Total links found: ${ foundSelectorLinks.length }` )
foundSelectorLinks.forEach( ( link, index ) => {
console.log( index + 1, `${ link.text } -> ${ decodeURI( buildWikipediaUrl( link.lang, link.title, isTouch, false ) ) }` )
console.log( index + 1, `${ link.text } -> ${ decodeURI( buildWikipediaUrl( link.lang, link.title, isMobile, false ) ) }` )
} )
console.groupEnd()
if ( detectLinks ) {
console.group( `Searching for links to Wikipedia, Total links found: ${ foundDetectLinks.length }` )
foundDetectLinks.forEach( ( link, index ) => {
console.log( index + 1, `${ link.text } -> ${ decodeURI( buildWikipediaUrl( link.lang, link.title, isTouch, false ) ) }` )
console.log( index + 1, `${ link.text } -> ${ decodeURI( buildWikipediaUrl( link.lang, link.title, isMobile, false ) ) }` )
} )
console.groupEnd()
}
Expand Down
40 changes: 20 additions & 20 deletions src/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { buildWikipediaUrl, getLinkIconSvg } from './utils'
import { getGallery } from './gallery'
import '../style/preview.less'

const getPreviewHeader = ( lang, isTouch, imageUrl = '', media = [] ) => {
const getPreviewHeader = ( lang, imageUrl = '', media = [] ) => {
const showThumbnail = imageUrl !== '' && media.length > 0 && media.length < 3
const thumbnail = imageUrl || media[ 0 ] && media[ 0 ].thumb
return `
<div class= "wikipediapreview-header ${ showThumbnail ? '' : 'wikipediapreview-header-no-thumb' }">
${ showThumbnail ? `<div class="wikipediapreview-header-image" style="${ `background-image:url('${ thumbnail }');background-size:cover;` }"></div>` : '' }
<div class="wikipediapreview-header-wordmark wikipediapreview-header-wordmark-${ lang }"></div>
${ isTouch ? '<div class="wikipediapreview-header-closebtn"></div>' : '' }
<div class="wikipediapreview-header-closebtn"></div>
</div>
`.trim()
}
Expand All @@ -29,31 +29,31 @@ const getPreviewBody = ( type, message, cta ) => {
`.trim()
}

const getReadOnWikiCta = ( lang, title, isTouch ) => {
return `<a href="${ buildWikipediaUrl( lang, title, isTouch ) }" target="_blank" class="wikipediapreview-footer-link-cta">${ msg( lang, 'read-on-wiki' ) }</a>`
const getReadOnWikiCta = ( lang, title, isMobile ) => {
return `<a href="${ buildWikipediaUrl( lang, title, isMobile ) }" target="_blank" class="wikipediapreview-footer-link-cta">${ msg( lang, 'read-on-wiki' ) }</a>`
}

const render = (
lang, isTouch, dir, headerContent, bodyContent, prefersColorScheme
lang, isMobile, dir, headerContent, bodyContent, prefersColorScheme
) => {
const colorScheme = prefersColorScheme === 'detect' ? '' : `wikipediapreview-${ prefersColorScheme }-theme`
return `
<div class="wikipediapreview ${ isTouch ? 'mobile' : '' } ${ colorScheme }" lang="${ lang }" dir="${ dir }">
<div class="wikipediapreview ${ isMobile ? 'mobile' : '' } ${ colorScheme }" lang="${ lang }" dir="${ dir }">
${ headerContent }
${ bodyContent }
</div>
`.trim()
}

const renderPreview = ( lang, data, isTouch, prefersColorScheme ) => {
const renderPreview = ( lang, data, isMobile, prefersColorScheme ) => {
const imageUrl = data.imgUrl,
bodyContent = `
${ getGallery( data.media ) }
<div class="wikipediapreview-body">
${ data.extractHtml }
<div class="wikipediapreview-footer">
<div class="wikipediapreview-footer-link">
<a href="${ buildWikipediaUrl( lang, data.title, isTouch ) }"
<a href="${ buildWikipediaUrl( lang, data.title, isMobile ) }"
class="wikipediapreview-footer-link-cta" target="_blank"
>
${ msg( lang, 'read-more' ) }
Expand All @@ -67,15 +67,15 @@ const renderPreview = ( lang, data, isTouch, prefersColorScheme ) => {

return render(
lang,
isTouch,
isMobile,
data.dir,
getPreviewHeader( lang, isTouch, imageUrl, data.media ),
getPreviewHeader( lang, imageUrl, data.media ),
bodyContent,
prefersColorScheme
)
}

const renderLoading = ( isTouch, lang, dir, prefersColorScheme ) => {
const renderLoading = ( isMobile, lang, dir, prefersColorScheme ) => {
const bodyContent = `
<div class="wikipediapreview-body wikipediapreview-body-loading">
<div class="wikipediapreview-body-loading-line larger"></div>
Expand All @@ -92,28 +92,28 @@ const renderLoading = ( isTouch, lang, dir, prefersColorScheme ) => {
<div class="wikipediapreview-footer-loading"></div>
`.trim()

return render( lang, isTouch, dir, getPreviewHeader( lang, isTouch ), bodyContent, prefersColorScheme )
return render( lang, isMobile, dir, getPreviewHeader( lang ), bodyContent, prefersColorScheme )
}

const renderError = ( isTouch, lang, title, dir, prefersColorScheme ) => {
const renderError = ( isMobile, lang, title, dir, prefersColorScheme ) => {
const message = `<span>${ msg( lang, 'preview-error-message' ) }</span>`
const cta = getReadOnWikiCta( lang, title, isTouch )
const cta = getReadOnWikiCta( lang, title, isMobile )

return render( lang, isTouch, dir, getPreviewHeader( lang, isTouch ), getPreviewBody( 'error', message, cta ), prefersColorScheme )
return render( lang, isMobile, dir, getPreviewHeader( lang ), getPreviewBody( 'error', message, cta ), prefersColorScheme )
}

const renderDisambiguation = ( isTouch, lang, title, dir, prefersColorScheme ) => {
const renderDisambiguation = ( isMobile, lang, title, dir, prefersColorScheme ) => {
const message = `<span>${ msg( lang, 'preview-disambiguation-message', title ) }</span>`
const cta = getReadOnWikiCta( lang, title, isTouch )
const cta = getReadOnWikiCta( lang, title, isMobile )

return render( lang, isTouch, dir, getPreviewHeader( lang, isTouch ), getPreviewBody( 'disambiguation', message, cta ), prefersColorScheme )
return render( lang, isMobile, dir, getPreviewHeader( lang ), getPreviewBody( 'disambiguation', message, cta ), prefersColorScheme )
}

const renderOffline = ( isTouch, lang, dir, prefersColorScheme ) => {
const renderOffline = ( isMobile, lang, dir, prefersColorScheme ) => {
const message = `<span>${ msg( lang, 'preview-offline-message' ) }</span>`
const cta = `<a>${ msg( lang, 'preview-offline-cta' ) }</a>`

return render( lang, isTouch, dir, getPreviewHeader( lang, isTouch ), getPreviewBody( 'offline', message, cta ), prefersColorScheme )
return render( lang, isMobile, dir, getPreviewHeader( lang ), getPreviewBody( 'offline', message, cta ), prefersColorScheme )
}

export { renderPreview, renderLoading, renderError, renderDisambiguation, renderOffline }
13 changes: 10 additions & 3 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ const getWikipediaAttrFromUrl = ( url ) => {
return null
}

const isTouch = 'ontouchstart' in window || ( navigator.maxTouchPoints > 0 ) ||
( navigator.msMaxTouchPoints > 0 )
/**
* Check if the device is a mobile device
*/
const isMobile = !!window.matchMedia( '( max-width: 768px )' ).matches

/**
* Check if the device is a touch device
*/
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0

const isOnline = () => window.navigator.onLine

Expand Down Expand Up @@ -132,5 +139,5 @@ const isVerticallyScrollable = ( element ) => {
export {
getWikipediaAttrFromUrl, isTouch, isOnline, getDir, buildMwApiUrl,
convertUrlToMobile, strip, sanitizeHTML, getDeviceSize, getAnalyticsQueryParam,
buildWikipediaUrl, version, logError, getElement, getLinkIconSvg, isVerticallyScrollable
buildWikipediaUrl, version, logError, getElement, getLinkIconSvg, isVerticallyScrollable, isMobile

Check warning on line 142 in src/utils.js

View workflow job for this annotation

GitHub Actions / build (21)

This line has a length of 102. Maximum allowed is 100

Check warning on line 142 in src/utils.js

View workflow job for this annotation

GitHub Actions / build (21)

This line has a length of 102. Maximum allowed is 100
}
Loading

0 comments on commit 29bc25b

Please sign in to comment.