Skip to content

Commit

Permalink
feat: update extended client:visible to use an object instead of a st…
Browse files Browse the repository at this point in the history
…ring (#9596)

* Revert "feat: support setting rootMargin for `client:visible` (#9363)"

This reverts commit 769826e.

* feat: update extended `client:visible` to use an object instead of a string

* Apply suggestions from code review

Co-authored-by: Nate Moore <[email protected]>

* test: add a test

* nit: comment

* test: write the test some other way to try to convince playwright

---------

Co-authored-by: Nate Moore <[email protected]>
  • Loading branch information
Princesseuh and natemoo-re authored Jan 4, 2024
1 parent da307e4 commit fbc2697
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 22 deletions.
10 changes: 0 additions & 10 deletions .changeset/stupid-peas-juggle.md

This file was deleted.

10 changes: 10 additions & 0 deletions .changeset/three-owls-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"astro": minor
---

Adds the ability to set a [`rootMargin`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) setting when using the `client:visible` directive. This allows a component to be hydrated when it is _near_ the viewport, rather than hydrated when it has _entered_ the viewport.

```astro
<!-- Load component when it's within 200px away from entering the viewport -->
<Component client:visible={{ rootMargin: "200px" }} />
```
14 changes: 13 additions & 1 deletion packages/astro/e2e/custom-client-directives.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test';
import { testFactory, waitForHydrate } from './test-utils.js';
import testAdapter from '../test/test-adapter.js';
import { testFactory, waitForHydrate } from './test-utils.js';

const test = testFactory({
root: './fixtures/custom-client-directives/',
Expand Down Expand Up @@ -89,4 +89,16 @@ function testClientDirectivesShared() {
// Hydrated, this should be 1
await expect(counterValue).toHaveText('1');
});

test('Client directives should be passed options correctly', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const optionsContent = page.locator('#client-has-options pre');
await waitForHydrate(page, optionsContent);

const clientOptions = page.locator('#options');
await expect(clientOptions).toHaveText(
'Passed options are: {"message":"Hello! I was passed as an option"}'
);
});
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { defineConfig } from 'astro/config';
import react from "@astrojs/react";
import { defineConfig } from 'astro/config';
import { fileURLToPath } from 'node:url';

export default defineConfig({
integrations: [astroClientClickDirective(), astroClientPasswordDirective(), react()],
integrations: [astroClientClickDirective(), astroClientPasswordDirective(), astroHasOptionsDirective(), react()],
});

function astroClientClickDirective() {
Expand Down Expand Up @@ -33,3 +33,17 @@ function astroClientPasswordDirective() {
}
};
}

function astroHasOptionsDirective() {
return {
name: 'astro-options',
hooks: {
'astro:config:setup': (opts) => {
opts.addClientDirective({
name: 'options',
entrypoint: fileURLToPath(new URL('./client-options.js', import.meta.url))
});
}
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Hydrate directly and write the passed options to the DOM
export default async (load, options) => {
const hydrate = await load();

const div = document.createElement('div');
div.id = 'options';
div.textContent = `Passed options are: ${JSON.stringify(options.value)}`;
document.body.appendChild(div);
await hydrate();
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
declare module 'astro' {
interface AstroClientDirectives {
'client:click'?: boolean
'client:password'?: string
'client:password'?: string
'client:options'?: { message: string }
}
}

// Make d.ts a module to similate common packaging setups where the entry `index.d.ts` would augment the types
export {}
export { }
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ import Counter from '../components/Counter.jsx';
<body>
<Counter id="client-click" client:click>client:click</Counter>
<Counter id="client-password" client:password="hunter2">client:password</Counter>
<Counter id="client-has-options" client:options={{message: "Hello! I was passed as an option"}}>client:options</Counter>
</body>
</html>
</html>
8 changes: 5 additions & 3 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,21 @@ export type {
export type { RemotePattern } from '../assets/utils/remotePattern.js';
export type { SSRManifest } from '../core/app/types.js';
export type {
AstroCookies,
AstroCookieSetOptions,
AstroCookieGetOptions,
AstroCookieSetOptions,
AstroCookies,
} from '../core/cookies/index.js';

export interface AstroBuiltinProps {
'client:load'?: boolean;
'client:idle'?: boolean;
'client:media'?: string;
'client:visible'?: string | boolean;
'client:visible'?: ClientVisibleOptions | boolean;
'client:only'?: boolean | string;
}

export type ClientVisibleOptions = Pick<IntersectionObserverInit, 'rootMargin'>;

export interface TransitionAnimation {
name: string; // The name of the keyframe
delay?: number | string;
Expand Down
9 changes: 6 additions & 3 deletions packages/astro/src/runtime/client/visible.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClientDirective } from '../../@types/astro.js';
import type { ClientDirective, ClientVisibleOptions } from '../../@types/astro.js';

/**
* Hydrate this component when one of it's children becomes visible
Expand All @@ -11,8 +11,11 @@ const visibleDirective: ClientDirective = (load, options, el) => {
await hydrate();
};

const ioOptions = {
rootMargin: typeof options.value === 'string' ? options.value : undefined,
const rawOptions =
typeof options.value === 'object' ? (options.value as ClientVisibleOptions) : undefined;

const ioOptions: IntersectionObserverInit = {
rootMargin: rawOptions?.rootMargin,
};

const io = new IntersectionObserver((entries) => {
Expand Down

0 comments on commit fbc2697

Please sign in to comment.