Skip to content

Commit dd3cde4

Browse files
authored
Merge pull request #216 from Automattic/add/clean-up-api
Hovercards: Add `hideOnTargetClick` option & Remove hovercards after calling the `detach()`
2 parents 89a67ba + 401d40d commit dd3cde4

7 files changed

Lines changed: 651 additions & 206 deletions

File tree

package-lock.json

Lines changed: 579 additions & 183 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/packages/hovercards/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ Determines whether the hovercard's placement should automatically flip when ther
187187

188188
Determines whether the hovercard's placement should automatically shift when there is not enough display space.
189189

190+
##### `hideOnTargetClick: boolean = false`
191+
192+
Hides the hovercard when its trigger element is clicked.
193+
190194
##### `offset: number = 10`
191195

192196
The offset of the hovercard relative to the target element, in pixels.

web/packages/hovercards/playground/core.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ addEventListener( 'DOMContentLoaded', () => {
99
placement: 'top',
1010
// To test the empty about me case
1111
myHash: '99c3338797c95c418d9996bd39931506',
12+
hideOnTargetClick: true,
1213
onCanShowHovercard: ( hash ) => {
1314
if ( hash === 'a2bb8d897bb538896708195dd9eb162f585654611c50a3a1c9a16a7b64f33270' ) {
1415
return false;
@@ -179,6 +180,11 @@ addEventListener( 'DOMContentLoaded', () => {
179180
)
180181
);
181182

183+
// To test detach
184+
document.getElementById( 'detach' )?.addEventListener( 'click', () => {
185+
hovercards.detach();
186+
} );
187+
182188
// To test hovercard skeleton
183189
document.getElementById( 'hovercard-skeleton' )?.appendChild( Hovercards.createHovercardSkeleton() );
184190

web/packages/hovercards/playground/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
height="60"
7777
/>
7878
<div id="attr" data-gravatar-hash="c3bb8d897bb538896708195dd9eb162f585654611c50a3a1c9a16a7b64f33270?s=60&d=retro&r=g">@WellyTest</div>
79+
<button id="detach">Detach</button>
7980
</div>
8081
<div id="react-app"></div>
8182
<div id="inline">

web/packages/hovercards/playground/react.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable import/no-unresolved */
22

33
import { createRoot } from 'react-dom/client';
4-
import React, { useRef, useEffect, useState } from 'react';
4+
import { useRef, useEffect, useState } from 'react';
55

66
import type { HovercardsProps } from '../dist/index.react.d';
77
import { useHovercards, Hovercards } from '../dist/index.react';
@@ -11,13 +11,15 @@ const props: HovercardsProps = {
1111
// attach: document.body,
1212
// placement: 'top',
1313
// ignoreSelector: '#grav-img-1',
14+
hideOnTargetClick: true,
1415
i18n: {
1516
'View profile →': 'Voir le profil →',
1617
},
1718
};
1819

1920
function App() {
2021
const { attach } = useHovercards( {
22+
hideOnTargetClick: true,
2123
// eslint-disable-next-line no-console
2224
onFetchProfileSuccess: ( hash ) => console.log( hash ),
2325
onCanShowHovercard: ( hash ) => {

web/packages/hovercards/src/core.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export type Options = Partial< {
112112
offset: number;
113113
autoFlip: boolean;
114114
autoShift: boolean;
115+
hideOnTargetClick: boolean;
115116
delayToShow: number;
116117
delayToHide: number;
117118
additionalClass: string;
@@ -133,6 +134,7 @@ interface HovercardRef {
133134
ref: HTMLElement;
134135
onEnter: ( e: MouseEvent ) => void;
135136
onLeave: ( e: MouseEvent ) => void;
137+
onClick: ( e: MouseEvent ) => void;
136138
}
137139

138140
const BASE_API_URL = 'https://api.gravatar.com/v3/profiles';
@@ -145,6 +147,7 @@ export default class Hovercards {
145147
_offset: number;
146148
_autoFlip: boolean;
147149
_autoShift: boolean;
150+
_hideOnTargetClick: boolean;
148151
_delayToShow: number;
149152
_delayToHide: number;
150153
_additionalClass: string;
@@ -166,9 +169,10 @@ export default class Hovercards {
166169

167170
constructor( {
168171
placement = 'right-start',
172+
offset = 10,
169173
autoFlip = true,
170174
autoShift = true,
171-
offset = 10,
175+
hideOnTargetClick = false,
172176
delayToShow = 500,
173177
delayToHide = 300,
174178
additionalClass = '',
@@ -183,9 +187,10 @@ export default class Hovercards {
183187
i18n = {},
184188
}: Options = {} ) {
185189
this._placement = placement;
190+
this._offset = offset;
186191
this._autoFlip = autoFlip;
187192
this._autoShift = autoShift;
188-
this._offset = offset;
193+
this._hideOnTargetClick = hideOnTargetClick;
189194
this._delayToShow = delayToShow;
190195
this._delayToHide = delayToHide;
191196
this._additionalClass = additionalClass;
@@ -206,10 +211,10 @@ export default class Hovercards {
206211
* @param {HTMLElement} target - The element to query.
207212
* @param {string} dataAttributeName - Data attribute name associated with Gravatar hashes.
208213
* @param {string} [ignoreSelector] - The selector to ignore certain elements.
209-
* @return {HTMLElement[]} - The queried hovercard refs.
214+
* @return {HovercardRef[]} - The queried hovercard refs.
210215
* @private
211216
*/
212-
_queryHovercardRefs( target: HTMLElement, dataAttributeName: string, ignoreSelector?: string ) {
217+
_queryHovercardRefs( target: HTMLElement, dataAttributeName: string, ignoreSelector?: string ): HovercardRef[] {
213218
let refs: HTMLElement[] = [];
214219
const camelAttrName = dataAttributeName.replace( /-([a-z])/g, ( g ) => g[ 1 ].toUpperCase() );
215220
const ignoreRefs = ignoreSelector ? Array.from( dc.querySelectorAll( ignoreSelector ) ) : [];
@@ -291,6 +296,7 @@ export default class Hovercards {
291296
...hovercardRef,
292297
onEnter: ( e: MouseEvent ) => this._handleMouseEnter( e, hovercardRef ),
293298
onLeave: ( e: MouseEvent ) => this._handleMouseLeave( e, hovercardRef ),
299+
onClick: () => this._handleMouseClick( hovercardRef ),
294300
} ) );
295301

296302
return this._hovercardRefs;
@@ -306,7 +312,10 @@ export default class Hovercards {
306312
* @param {Object} [options.i18n] - The i18n object.
307313
* @return {HTMLDivElement} - The created hovercard element.
308314
*/
309-
static createHovercard: CreateHovercard = ( profileData, { additionalClass, myHash, i18n = {} } = {} ) => {
315+
static createHovercard: CreateHovercard = (
316+
profileData,
317+
{ additionalClass, myHash, i18n = {} } = {}
318+
): HTMLDivElement => {
310319
const {
311320
hash,
312321
avatarUrl,
@@ -658,7 +667,7 @@ export default class Hovercards {
658667
* @param {string} [options.additionalClass] - Additional CSS class for the skeleton hovercard.
659668
* @return {HTMLDivElement} - The created skeleton hovercard element.
660669
*/
661-
static createHovercardSkeleton: CreateHovercardSkeleton = ( { additionalClass } = {} ) => {
670+
static createHovercardSkeleton: CreateHovercardSkeleton = ( { additionalClass } = {} ): HTMLDivElement => {
662671
const hovercard = dc.createElement( 'div' );
663672
hovercard.className = `gravatar-hovercard gravatar-hovercard--skeleton${
664673
additionalClass ? ` ${ additionalClass }` : ''
@@ -699,7 +708,7 @@ export default class Hovercards {
699708
avatarUrl,
700709
message,
701710
{ avatarAlt = 'Avatar', additionalClass, additionalMessage = '' } = {}
702-
) => {
711+
): HTMLDivElement => {
703712
const hovercard = dc.createElement( 'div' );
704713
hovercard.className = `gravatar-hovercard gravatar-hovercard--error${
705714
additionalClass ? ` ${ additionalClass }` : ''
@@ -726,7 +735,7 @@ export default class Hovercards {
726735
* @return {void}
727736
* @private
728737
*/
729-
_showHovercard( { id, hash, params, ref }: HovercardRef ) {
738+
_showHovercard( { id, hash, params, ref }: HovercardRef ): void {
730739
const timeoutId = setTimeout( () => {
731740
if ( dc.getElementById( id ) ) {
732741
return;
@@ -878,32 +887,33 @@ export default class Hovercards {
878887
/**
879888
* Waits for a specified delay and hides the hovercard.
880889
*
881-
* @param {string} id - The ID associated with the hovercard.
890+
* @param {string} id - The ID associated with the hovercard.
891+
* @param {number} [delay] - The delay in milliseconds before hiding the hovercard.
882892
* @return {void}
883893
* @private
884894
*/
885-
_hideHovercard( id: string ) {
895+
_hideHovercard( id: string, delay = this._delayToHide ): void {
886896
const timeoutId = setTimeout( () => {
887897
const hovercard = dc.getElementById( id );
888898

889899
if ( hovercard ) {
890900
hovercard.remove();
891901
this._onHovercardHidden( id, hovercard as HTMLDivElement );
892902
}
893-
}, this._delayToHide );
903+
}, delay );
894904

895905
this._hideHovercardTimeoutIds.set( id, timeoutId );
896906
}
897907

898908
/**
899909
* Handles the mouseenter event for hovercard refs.
900910
*
901-
* @param {MouseEvent} e - The mouseenter event object.
902-
* @param hovercardRef - The hovercard ref object.
911+
* @param {MouseEvent} e - The mouseenter event object.
912+
* @param {HovercardRef} hovercardRef - The hovercard ref object.
903913
* @return {void}
904914
* @private
905915
*/
906-
_handleMouseEnter( e: MouseEvent, hovercardRef: HovercardRef ) {
916+
_handleMouseEnter( e: MouseEvent, hovercardRef: HovercardRef ): void {
907917
if ( 'ontouchstart' in dc ) {
908918
return;
909919
}
@@ -918,13 +928,13 @@ export default class Hovercards {
918928
/**
919929
* Handles the mouseleave event for hovercard refs.
920930
*
921-
* @param {MouseEvent} e - The mouseleave event object.
922-
* @param hovercardRef - The hovercard ref object.
923-
* @param hovercardRef.id - The ID associated with the hovercard.
931+
* @param {MouseEvent} e - The mouseleave event object.
932+
* @param {HovercardRef} hovercardRef - The hovercard ref object.
933+
* @param {string} hovercardRef.id - The ID associated with the hovercard.
924934
* @return {void}
925935
* @private
926936
*/
927-
_handleMouseLeave( e: MouseEvent, { id }: HovercardRef ) {
937+
_handleMouseLeave( e: MouseEvent, { id }: HovercardRef ): void {
928938
if ( 'ontouchstart' in dc ) {
929939
return;
930940
}
@@ -935,16 +945,33 @@ export default class Hovercards {
935945
this._hideHovercard( id );
936946
}
937947

948+
/**
949+
* Handles the click event for hovercard refs.
950+
*
951+
* @param {HovercardRef} hovercardRef - The hovercard ref object.
952+
* @param {string} hovercardRef.id - The ID associated with the hovercard.
953+
* @return {void}
954+
* @private
955+
*/
956+
_handleMouseClick( { id }: HovercardRef ): void {
957+
if ( 'ontouchstart' in dc || ! this._hideOnTargetClick ) {
958+
return;
959+
}
960+
961+
clearInterval( this._showHovercardTimeoutIds.get( id ) );
962+
this._hideHovercard( id, 0 );
963+
}
964+
938965
/**
939966
* Attaches event listeners on or within the target element.
940967
*
941968
* @param {HTMLElement} target - The target element to set.
942969
* @param {Object} [options={}] - The optional parameters.
943-
* @param options.dataAttributeName - Data attribute name associated with Gravatar hashes.
944-
* @param options.ignoreSelector - The selector to ignore certain elements.
970+
* @param {string} options.dataAttributeName - Data attribute name associated with Gravatar hashes.
971+
* @param {string} options.ignoreSelector - The selector to ignore certain elements.
945972
* @return {void}
946973
*/
947-
attach: Attach = ( target, { dataAttributeName = 'gravatar-hash', ignoreSelector } = {} ) => {
974+
attach: Attach = ( target, { dataAttributeName = 'gravatar-hash', ignoreSelector } = {} ): void => {
948975
if ( ! target ) {
949976
return;
950977
}
@@ -954,6 +981,7 @@ export default class Hovercards {
954981
this._queryHovercardRefs( target, dataAttributeName, ignoreSelector ).forEach( ( hovercardRef ) => {
955982
hovercardRef.ref.addEventListener( 'mouseenter', hovercardRef.onEnter );
956983
hovercardRef.ref.addEventListener( 'mouseleave', hovercardRef.onLeave );
984+
hovercardRef.ref.addEventListener( 'click', hovercardRef.onClick );
957985
} );
958986
};
959987

@@ -962,14 +990,19 @@ export default class Hovercards {
962990
*
963991
* @return {void}
964992
*/
965-
detach: Detach = () => {
993+
detach: Detach = (): void => {
966994
if ( ! this._hovercardRefs.length ) {
967995
return;
968996
}
969997

970998
this._hovercardRefs.forEach( ( hovercardRef ) => {
971999
hovercardRef.ref.removeEventListener( 'mouseenter', hovercardRef.onEnter );
9721000
hovercardRef.ref.removeEventListener( 'mouseleave', hovercardRef.onLeave );
1001+
hovercardRef.ref.removeEventListener( 'click', hovercardRef.onClick );
1002+
1003+
// Clear the hovercard show timeout and remove the hovercard element.
1004+
clearInterval( this._showHovercardTimeoutIds.get( hovercardRef.id ) );
1005+
dc.getElementById( hovercardRef.id )?.remove();
9731006
} );
9741007

9751008
this._hovercardRefs = [];

web/packages/hovercards/src/use-hovercards.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function useHovercards( {
1717
offset,
1818
autoFlip,
1919
autoShift,
20+
hideOnTargetClick,
2021
delayToShow,
2122
delayToHide,
2223
additionalClass,
@@ -47,6 +48,7 @@ export default function useHovercards( {
4748
offset,
4849
autoFlip,
4950
autoShift,
51+
hideOnTargetClick,
5052
delayToShow,
5153
delayToHide,
5254
additionalClass,
@@ -65,6 +67,7 @@ export default function useHovercards( {
6567
offset,
6668
autoFlip,
6769
autoShift,
70+
hideOnTargetClick,
6871
delayToShow,
6972
delayToHide,
7073
additionalClass,

0 commit comments

Comments
 (0)