Merge pull request #262 from reuters-graphics/mf-visible

Updates Visible
This commit is contained in:
MinamiFunakoshiTR 2025-04-15 12:22:07 -05:00 committed by GitHub
commit 6fc30bc419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 65 additions and 61 deletions

View file

@ -0,0 +1,31 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as VisibleStories from './Visible.stories.svelte';
<Meta of={VisibleStories} />
# Visible
The `Visible` component wraps around other components or HTML elements and uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to determine if they are visible on the page.
This is useful for lazy loading elements, especially expensive media files or components that fetch lots of data. You can also use it trigger animations or play media once a reader scrolls a component into view.
By default, `visible` will switch between `false` and `true` whenever the element is in our out of view. To trigger this just once, set the prop `once` to `true`. This is useful for loading expensive media when they first come into view and then keeping them around once they're loaded.
> **Note:** Don't use this for content that's "above the fold" at the top of the page. That'll just slow down the first load of important visible content.
```svelte
<script>
import { Visible } from '@reuters-graphics/graphics-components';
</script>
<Visible let:visible>
{#if visible}
<p>Visible!</p>
{:else}
<p>Not yet visible.</p>
{/if}
</Visible>
```
<Canvas of={VisibleStories.Demo} />

View file

@ -1,36 +1,22 @@
<script module lang="ts"> <script module lang="ts">
// @ts-ignore raw import { defineMeta } from '@storybook/addon-svelte-csf';
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore raw
import defaultSnippet from './stories/snippets/default.svelte?raw';
import Visible from './Visible.svelte'; import Visible from './Visible.svelte';
import { withSource, withComponentDocs } from '$lib/docs/utils/withParams.js'; const { Story } = defineMeta({
export const meta = {
title: 'Components/Utilities/Visible', title: 'Components/Utilities/Visible',
component: Visible, component: Visible,
...withComponentDocs(componentDocs), });
};
</script> </script>
<script> <Story name="Demo" tags={['!autodocs', '!dev']}>
import { Template, Story } from '@storybook/addon-svelte-csf'; <Visible>
</script> {#snippet children(visible)}
{#if visible}
<Template> <p>Visible!</p>
{#snippet children({ args })} {:else}
<Visible {...args}> <p>Not yet visible.</p>
{#snippet children({ visible })} {/if}
{#if visible} {/snippet}
<p>Visible!</p> </Visible>
{:else} </Story>
<p>Not yet visible.</p>
{/if}
{/snippet}
</Visible>
{/snippet}
</Template>
<Story name="Default" {...withSource({ svelte: defaultSnippet })} />

View file

@ -1,6 +1,6 @@
<!-- @component `Visible` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-utilities-visible--docs) --> <!-- @component `Visible` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-utilities-visible--docs) -->
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount, type Snippet } from 'svelte';
interface Props { interface Props {
/** /**
@ -19,7 +19,7 @@
right?: number; right?: number;
/** Set the Intersection Observer [threshold](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#threshold). */ /** Set the Intersection Observer [threshold](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#threshold). */
threshold?: number; threshold?: number;
children?: import('svelte').Snippet<[any]>; children?: Snippet<[boolean]>;
} }
let { let {
@ -33,15 +33,16 @@
}: Props = $props(); }: Props = $props();
let visible = $state(false); let visible = $state(false);
let container: HTMLElement = $state(); let container: HTMLElement | undefined = $state(undefined);
onMount(() => { onMount(() => {
if (typeof IntersectionObserver !== 'undefined') { if (typeof IntersectionObserver !== 'undefined') {
const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`; const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
visible = entries[0].isIntersecting; visible = entries[0].isIntersecting;
if (visible && once) { if (visible && once && container) {
observer.unobserve(container); observer.unobserve(container);
} }
}, },
@ -50,16 +51,20 @@
threshold, threshold,
} }
); );
observer.observe(container); if (container) observer.observe(container);
return () => observer.unobserve(container); return () => {
if (container) observer.observe(container);
};
} }
function handler() { function handler() {
const bcr = container.getBoundingClientRect(); if (container) {
visible = const bcr = container.getBoundingClientRect();
bcr.bottom + bottom > 0 && visible =
bcr.right + right > 0 && bcr.bottom + bottom > 0 &&
bcr.top - top < window.innerHeight && bcr.right + right > 0 &&
bcr.left - left < window.innerWidth; bcr.top - top < window.innerHeight &&
bcr.left - left < window.innerWidth;
}
if (visible && once) { if (visible && once) {
window.removeEventListener('scroll', handler); window.removeEventListener('scroll', handler);
} }
@ -70,6 +75,7 @@
</script> </script>
<div bind:this={container}> <div bind:this={container}>
<!-- An element or component --> {#if children}
{@render children?.({ visible })} {@render children(visible)}
{/if}
</div> </div>

View file

@ -1,19 +0,0 @@
Wrap components or other HTML elements to determine if they are visible on the page using the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
This is really useful for lazy loading elements, especially expensive media files or components that fetch lots of data. You can also use it trigger animations or play media once a reader scrolls a component into view.
> **Pro tip:** Don't use this for content that's "above the fold" at the top of the page. That'll just slow down the first load of important visible content.
```svelte
<script>
import { Visible } from '@reuters-graphics/graphics-components';
</script>
<Visible let:visible>
{#if visible}
<p>Visible!</p>
{:else}
<p>Not yet visible.</p>
{/if}
</Visible>
```