Skip to content

Commit

Permalink
v1.1.0: Add support for multiple hover targets
Browse files Browse the repository at this point in the history
Resolves issue #3
  • Loading branch information
Gyanreyer committed Jan 5, 2024
1 parent 6c22d90 commit 277398a
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 33 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,22 +291,37 @@ If you wish, you may customize this timeout duration by setting your own value f

#### hover-target

The optional `"hover-target"` attribute can be used to provide a selector string for a custom element which the component should watch for hover interactions. If a hover target is not set, the component will use its root element as the hover target.
The optional `"hover-target"` attribute can be used to provide a selector string for element(s) which the component should watch for hover interactions. If a hover target is not set, the component will use its root element as the hover target.

Note that if you provide a selector which matches multiple elements in the document, they will all be added as hover targets.

The component's hover target can also be accessed and updated in JS with the `hoverTarget` property.
This property may be a single `Element` instance, or an iterable of `Element` instances; a manually constructed array, a `NodeList` returned by `querySelectorAll`, or an HTMLCollection returned by `getElementsByClassName` are all acceptable.

```html
<!-- A single hover target -->
<div id="hover-on-me">Hover on me to start playing!</div>
<hover-video-player hover-target="#hover-on-me">
<video src="video.mp4" />
</hover-video-player>

<!-- Multiple hover targets -->
<div class="hover-target">You can hover on me to play</div>
<div class="hover-target">You can also hover on me!</div>
<hover-video-player hover-target=".hover-target">
<video src="video.mp4" />
</hover-video-player>
```

Setting with JS:

```js
const player = document.querySelector("hover-video-player");
player.hoverTarget = document.getElementById("#hover-on-me");
// Setting a single hover target element
player.hoverTarget = document.getElementById("hover-on-me");

// Setting multiple hover targets
player.hoverTarget = document.querySelectorAll(".hover-target");
```

#### restart-on-pause
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hover-video-player",
"version": "1.0.4",
"version": "1.1.0",
"description": "A web component for rendering videos that play on hover, including support for mouse and touch events and a simple API for adding thumbnails and loading states.",
"repository": {
"type": "git",
Expand Down
67 changes: 40 additions & 27 deletions src/hover-video-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,31 @@ export default class HoverVideoPlayer extends HTMLElement {
// The element which we will watch for hover events to start and stop video playback.
// This maps to the element with the selector set in the "hover-target" attribute if applicable,
// or else will just be this component's host element.
private _hoverTarget: Element = this;
private _hoverTarget: Element | Iterable<Element> = this;
/**
* hoverTarget setter allows you to change the hover target element programmatically if you don't
* want to deal with the `hover-target` selector attribute.
* You can set a single element, a NodeList returned from querySelectorAll, or an array of elements.
*
* @example
* const player = document.querySelector('hover-video-player');
* player.hoverTarget = document.querySelector('.my-hover-target');
* player.hoverTarget = document.querySelectorAll('.my-hover-target');
*/
public set hoverTarget(newHoverTarget: Element | null) {
public set hoverTarget(newHoverTarget: Element | Iterable<Element> | null) {
// Remove any `hover-target` attribute to reduce confusion after setting the hover target programmatically
this.removeAttribute("hover-target");
this._setHoverTarget(newHoverTarget || this);
}
/**
* hoverTarget getter returns the current hover target element that the player is using.
*/
public get hoverTarget(): Element {
public get hoverTarget(): Element | Iterable<Element> {
return this._hoverTarget;
}

// The element which the user is currently hovering over. Null if the user is not hovering over a target.
private _activeHoverTarget: Element | null = null;

// Whether the user is currently hovering over the hover target.
public isHovering: boolean = false;
// The current playback state of the player.
Expand Down Expand Up @@ -178,7 +182,7 @@ export default class HoverVideoPlayer extends HTMLElement {
this.shadowRoot?.addEventListener("slotchange", this._onSlotChange);

if (!this.hasAttribute("hover-target")) {
this._addHoverTargetListeners(this._hoverTarget);
this._addHoverTargetListeners();
}

if (this.getAttribute("sizing-mode") === null) {
Expand All @@ -198,7 +202,7 @@ export default class HoverVideoPlayer extends HTMLElement {
this._cleanupTimeoutIDs();

this.removeEventListener("slotchange", this._onSlotChange);
this._removeHoverTargetListeners(this._hoverTarget);
this._removeHoverTargetListeners();
window.removeEventListener("touchstart", this._onTouchOutsideOfHoverTarget);
}

Expand Down Expand Up @@ -248,7 +252,8 @@ export default class HoverVideoPlayer extends HTMLElement {
/**
* Handler updates state and starts video playback when the user hovers over the hover target.
*/
private _onHoverStart() {
private _onHoverStart(event: Event) {
this._activeHoverTarget = event.currentTarget as Element;
if (!this.isHovering) {
this._cleanupTimeoutIDs();

Expand All @@ -263,6 +268,7 @@ export default class HoverVideoPlayer extends HTMLElement {
* Handler updates state and pauses video playback when the user stops hovering over the hover target.
*/
private _onHoverEnd() {
this._activeHoverTarget = null;
if (this.isHovering) {
this._cleanupTimeoutIDs();

Expand All @@ -281,9 +287,9 @@ export default class HoverVideoPlayer extends HTMLElement {
if (!this.isHovering) return;

if (
!this._hoverTarget ||
!this._activeHoverTarget ||
!(event.target instanceof Node) ||
!this._hoverTarget.contains(event.target)
!this._activeHoverTarget.contains(event.target)
) {
this._onHoverEnd();
}
Expand All @@ -292,23 +298,29 @@ export default class HoverVideoPlayer extends HTMLElement {
/**
* Hooks up event listeners to the hover target so it can start listening for hover events that will trigger playback.
*/
private _addHoverTargetListeners(hoverTarget: Element) {
hoverTarget.addEventListener("mouseenter", this._onHoverStart);
hoverTarget.addEventListener("mouseleave", this._onHoverEnd);
hoverTarget.addEventListener("focus", this._onHoverStart);
hoverTarget.addEventListener("blur", this._onHoverEnd);
hoverTarget.addEventListener("touchstart", this._onHoverStart, {
passive: true,
});
private _addHoverTargetListeners() {
const hoverTargets = Symbol.iterator in this._hoverTarget ? this._hoverTarget as Iterable<Element> : [this._hoverTarget as Element];

for (const hoverTarget of hoverTargets) {
hoverTarget.addEventListener("mouseenter", this._onHoverStart);
hoverTarget.addEventListener("mouseleave", this._onHoverEnd);
hoverTarget.addEventListener("focus", this._onHoverStart);
hoverTarget.addEventListener("blur", this._onHoverEnd);
hoverTarget.addEventListener("touchstart", this._onHoverStart, {
passive: true,
});
}
}

/**
* Cleans up event listeners from a hover target element if the hover target has changed or the component has been disconnected.
*
* @param {HTMLElement} hoverTarget The hover target element to remove event listeners from.
*/
private _removeHoverTargetListeners(hoverTarget: Element | null) {
if (hoverTarget) {
private _removeHoverTargetListeners() {
const hoverTargets = Symbol.iterator in this._hoverTarget ? this._hoverTarget as Iterable<Element> : [this._hoverTarget as Element];

for (const hoverTarget of hoverTargets) {
hoverTarget.removeEventListener("mouseenter", this._onHoverStart);
hoverTarget.removeEventListener("mouseleave", this._onHoverEnd);
hoverTarget.removeEventListener("focus", this._onHoverStart);
Expand Down Expand Up @@ -511,17 +523,17 @@ export default class HoverVideoPlayer extends HTMLElement {
}

/**
* Updates the componen't shover target element, cleans up event listeners on the
* Updates the component's hover target element, cleans up event listeners on the
* old hover target, and sets up new listeners on the new hover target.
*
* @param {Element} newHoverTarget
*/
private _setHoverTarget(newHoverTarget: Element) {
private _setHoverTarget(newHoverTarget: Iterable<Element> | Element) {
if (newHoverTarget === this._hoverTarget) return;

this._removeHoverTargetListeners(this._hoverTarget);
this._removeHoverTargetListeners();
this._hoverTarget = newHoverTarget;
this._addHoverTargetListeners(this._hoverTarget);
this._addHoverTargetListeners();
}

/**
Expand All @@ -536,13 +548,14 @@ export default class HoverVideoPlayer extends HTMLElement {
return;
}

const hoverTarget = document.querySelector(selector);
if (!hoverTarget) {
const hoverTarget = document.querySelectorAll(selector);
if (hoverTarget.length === 0) {
console.error(`hover-video-player failed to find a hover target element with the selector "${selector}".`);
this._setHoverTarget(this);
} else {
this._setHoverTarget(hoverTarget);
}
this._setHoverTarget(hoverTarget || this);
}

}

if (!customElements.get('hover-video-player')) {
Expand Down
15 changes: 13 additions & 2 deletions tests/hoverTarget.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
</style>
</head>
<body>
<div id="hover-target-1"></div>
<div id="hover-target-2"></div>
<div id="hover-target-1" class="hover-target"></div>
<div id="hover-target-2" class="hover-target"></div>
<hover-video-player
hover-target="#hover-target-1"
data-testid="has-initial-hover-target"
Expand All @@ -42,5 +42,16 @@
loop
></video>
</hover-video-player>
<hover-video-player
hover-target=".hover-target"
data-testid="multiple-hover-targets"
>
<video
src="/tests/assets/BigBuckBunny.mp4"
playsinline
muted
loop
></video>
</hover-video-player>
</body>
</html>
33 changes: 32 additions & 1 deletion tests/hoverTarget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ const expectComponentHasHoverTarget = async (page: Page, componentLocator: Locat
// The component's `hoverTarget` property should match the expected element
expect(await page.evaluate(({
component, expectedHoverTarget,
}) => (component as any).hoverTarget === expectedHoverTarget, { component: await componentLocator.elementHandle(), expectedHoverTarget: await expectedElementLocator.elementHandle() })).toBe(true);
}) => {
const hoverTarget = (component as any).hoverTarget;
if (Symbol.iterator in hoverTarget) {
return Array.from(hoverTarget).includes(expectedHoverTarget);
} else {
return hoverTarget === expectedHoverTarget;
}
}, { component: await componentLocator.elementHandle(), expectedHoverTarget: await expectedElementLocator.elementHandle() })).toBe(true);
const video = await componentLocator.locator("video");
// The video should be paused
await expect(video).toHaveJSProperty("paused", true);
Expand Down Expand Up @@ -92,4 +99,28 @@ test("hoverTarget can be updated for a component without an initial hover target
// Set the hoverTarget JS property to null to revert to using the component host element as the hover target
await componentWithNoInitialHoverTarget.evaluate((componentElement: any) => { componentElement.hoverTarget = null; });
await expectComponentHasHoverTarget(page, componentWithNoInitialHoverTarget, componentWithNoInitialHoverTarget);
});

test("Multiple hoverTargets can be set at the same time for a single component", async ({ page }) => {
await page.goto("/tests/hoverTarget.html");

const [componentWithMultipleHoverTargets, hoverTarget1, hoverTarget2] = await Promise.all([
page.locator("[data-testid='multiple-hover-targets']"),
page.locator("#hover-target-1"),
page.locator("#hover-target-2"),
]);

// The component should have two hover targets at the same time
expect(await componentWithMultipleHoverTargets.evaluate((componentElement: any) => componentElement.hoverTarget.length)).toBe(2);
await expectComponentHasHoverTarget(page, componentWithMultipleHoverTargets, hoverTarget1);
await expectComponentHasHoverTarget(page, componentWithMultipleHoverTargets, hoverTarget2);

// Reset the component hover target to the host element
await componentWithMultipleHoverTargets.evaluate((componentElement: any) => { componentElement.hoverTarget = null });
await expectComponentHasHoverTarget(page, componentWithMultipleHoverTargets, componentWithMultipleHoverTargets);

// Programmatically set the hover target to both hoverTarget1 and hoverTarget2 (they both have a .hover-target class)
await componentWithMultipleHoverTargets.evaluate((componentElement: any) => { componentElement.hoverTarget = document.getElementsByClassName("hover-target"); });
await expectComponentHasHoverTarget(page, componentWithMultipleHoverTargets, hoverTarget1);
await expectComponentHasHoverTarget(page, componentWithMultipleHoverTargets, hoverTarget2);
});

0 comments on commit 277398a

Please sign in to comment.