Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: IsInViewport #181

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/empty-candles-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": minor
---

feat: `IsInViewport`
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { MaybeGetter } from "$lib/internal/types.js";
import {
useIntersectionObserver,
type UseIntersectionObserverOptions,
} from "../useIntersectionObserver/useIntersectionObserver.svelte.js";

export type IsInViewportOptions = UseIntersectionObserverOptions;

/**
* Tracks if an element is visible within the current viewport.
*
* @see {@link https://runed.dev/docs/utilities/is-in-viewport}
*/
export class IsInViewport {
#isInViewport = $state(false);

constructor(node: MaybeGetter<HTMLElement | null | undefined>, options?: IsInViewportOptions) {
useIntersectionObserver(
node,
(intersectionObserverEntries) => {
let isIntersecting = this.#isInViewport;
let latestTime = 0;
for (const entry of intersectionObserverEntries) {
if (entry.time >= latestTime) {
latestTime = entry.time;
isIntersecting = entry.isIntersecting;
}
}
this.#isInViewport = isIntersecting;
},
options
);
}

get current() {
return this.#isInViewport;
}
}
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/IsInViewport/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./IsInViewport.svelte.js";
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from "./FiniteStateMachine/index.js";
export * from "./PersistedState/index.js";
export * from "./useGeolocation/index.js";
export * from "./Context/index.js";
export * from "./IsInViewport/index.js";
49 changes: 49 additions & 0 deletions sites/docs/src/content/utilities/is-in-viewport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: IsInViewport
description: N/A
category: Utilities
---

<script>
import Demo from '$lib/components/demos/is-in-viewport.svelte';
</script>

`IsInViewport` uses the [`useIntersectionObserver`](/docs/utilities/use-intersection-observer)
utility to track if an element is visible within the current viewport.

It accepts an element or getter that returns an element and an optional `options` object that aligns
with the [`useIntersectionObserver`](/docs/utilities/use-intersection-observer) utility options.

## Demo

<Demo />

## Usage

```svelte
<script lang="ts">
import { IsInViewport } from "runed";

let targetNode = $state<HTMLElement>()!;
const inViewport = new IsInViewport(() => targetNode);
</script>

<p bind:this={targetNode}>Target node</p>

<p>Target node in viewport: {inViewport.current}</p>
```

## Type Definition

```ts
import { type UseIntersectionObserverOptions } from "runed";
export type IsInViewportOptions = UseIntersectionObserverOptions;

export declare class IsInViewport {
constructor(node: MaybeGetter<HTMLElement | null | undefined>, options?: IsInViewportOptions);
get current(): boolean;
}
```

<!-- Ensure the page can scroll so the target can be outside of the viewport -->
<div class="h-80"></div>
26 changes: 26 additions & 0 deletions sites/docs/src/lib/components/demos/is-in-viewport.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import { IsInViewport } from "runed";
import { DemoContainer } from "@svecodocs/kit";

let targetNode = $state<HTMLElement>()!;
const inViewport = new IsInViewport(() => targetNode);
</script>

<DemoContainer>
<p bind:this={targetNode}>Target node</p>
<p class="text-muted-foreground text-sm italic">Scroll down to observe the behavior</p>
</DemoContainer>

<div
class="bg-background fixed bottom-8 right-8 z-20 flex items-center rounded-lg border p-4 text-sm"
>
<p>
Target node is <span
class="font-medium text-red-500 data-[in-viewport]:text-green-500"
data-in-viewport={inViewport.current ? "" : undefined}
>
{inViewport.current ? " in " : " out of "}
</span>
viewport
</p>
</div>