Skip to content

Commit

Permalink
feat: add ElementHandle.scrollIntoView
Browse files Browse the repository at this point in the history
  • Loading branch information
OrKoN committed Apr 11, 2023
1 parent 656b562 commit a0219fd
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 41 deletions.
1 change: 1 addition & 0 deletions docs/api/puppeteer.elementhandle.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The constructor for this class is marked as internal. Third-party code should no
| [isIntersectingViewport(this, options)](./puppeteer.elementhandle.isintersectingviewport.md) | | Resolves to true if the element is visible in the current viewport. If an element is an SVG, we check if the svg owner element is in the viewport instead. See https://crbug.com/963246. |
| [press(key, options)](./puppeteer.elementhandle.press.md) | | Focuses the element, and then uses [Keyboard.down()](./puppeteer.keyboard.down.md) and [Keyboard.up()](./puppeteer.keyboard.up.md). |
| [screenshot(this, options)](./puppeteer.elementhandle.screenshot.md) | | This method scrolls element into view if needed, and then uses [Page.screenshot()](./puppeteer.page.screenshot_2.md) to take a screenshot of the element. If the element is detached from DOM, the method throws an error. |
| [scrollIntoView(this)](./puppeteer.elementhandle.scrollintoview.md) | | Scrolls the element into view using either the automation protocol client or by calling element.scrollIntoView. |
| [select(values)](./puppeteer.elementhandle.select.md) | | Triggers a <code>change</code> and <code>input</code> event once all the provided options have been selected. If there's no <code>&lt;select&gt;</code> element matching <code>selector</code>, the method throws an error. |
| [tap(this)](./puppeteer.elementhandle.tap.md) | | This method scrolls element into view if needed, and then uses [Touchscreen.tap()](./puppeteer.touchscreen.tap.md) to tap in the center of the element. If the element is detached from DOM, the method throws an error. |
| [toElement(tagName)](./puppeteer.elementhandle.toelement.md) | | Converts the current handle to the given element type. |
Expand Down
25 changes: 25 additions & 0 deletions docs/api/puppeteer.elementhandle.scrollintoview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
sidebar_label: ElementHandle.scrollIntoView
---

# ElementHandle.scrollIntoView() method

Scrolls the element into view using either the automation protocol client or by calling element.scrollIntoView.

#### Signature:

```typescript
class ElementHandle {
scrollIntoView(this: ElementHandle<Element>): Promise<void>;
}
```

## Parameters

| Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ----------- |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |

**Returns:**

Promise&lt;void&gt;
31 changes: 31 additions & 0 deletions packages/puppeteer-core/src/api/ElementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,27 @@ export class ElementHandle<
throw new Error('Not implemented');
}

/**
* @internal
*/
protected async assertConnectedElement(): Promise<void> {
const error = await this.evaluate(
async (element): Promise<string | undefined> => {
if (!element.isConnected) {
return 'Node is detached from document';
}
if (element.nodeType !== Node.ELEMENT_NODE) {
return 'Node is not of type HTMLElement';
}
return;
}
);

if (error) {
throw new Error(error);
}
}

/**
* Resolves to true if the element is visible in the current viewport. If an
* element is an SVG, we check if the svg owner element is in the viewport
Expand All @@ -822,6 +843,8 @@ export class ElementHandle<
threshold?: number;
}
): Promise<boolean> {
await this.assertConnectedElement();

const {threshold = 0} = options ?? {};
const svgHandle = await this.#asSVGElementHandle(this);
const intersectionTarget: ElementHandle<Element> = svgHandle
Expand All @@ -846,6 +869,14 @@ export class ElementHandle<
}
}

/**
* Scrolls the element into view using either the automation protocol client
* or by calling element.scrollIntoView.
*/
async scrollIntoView(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}

/**
* Returns true if an element is an SVGElement (included svg, path, rect
* etc.).
Expand Down
68 changes: 27 additions & 41 deletions packages/puppeteer-core/src/common/ElementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,56 +231,42 @@ export class CDPElementHandle<
return this.#frameManager.frame(nodeInfo.node.frameId);
}

async #scrollIntoViewIfNeeded(
override async scrollIntoView(
this: CDPElementHandle<Element>
): Promise<void> {
const error = await this.evaluate(
async (element): Promise<string | undefined> => {
if (!element.isConnected) {
return 'Node is detached from document';
}
if (element.nodeType !== Node.ELEMENT_NODE) {
return 'Node is not of type HTMLElement';
}
return;
}
);

if (error) {
throw new Error(error);
}
await this.assertConnectedElement();

try {
await this.client.send('DOM.scrollIntoViewIfNeeded', {
objectId: this.remoteObject().objectId,
});
} catch (_err) {
} catch (error) {
debugError(error);
// Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
await this.evaluate(
async (element, pageJavascriptEnabled): Promise<void> => {
const visibleRatio = async () => {
return await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0]!.intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
};
if (!pageJavascriptEnabled || (await visibleRatio()) !== 1.0) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// @ts-expect-error Chrome still supports behavior: instant but
// it's not in the spec so TS shouts We don't want to make this
// breaking change in Puppeteer yet so we'll ignore the line.
behavior: 'instant',
});
}
},
this.#page.isJavaScriptEnabled()
);
await this.evaluate(async (element): Promise<void> => {
element.scrollIntoView({
block: 'center',
inline: 'center',
// @ts-expect-error Chrome still supports behavior: instant but
// it's not in the spec so TS shouts We don't want to make this
// breaking change in Puppeteer yet so we'll ignore the line.
behavior: 'instant',
});
});
}
}

async #scrollIntoViewIfNeeded(
this: CDPElementHandle<Element>
): Promise<void> {
if (
await this.isIntersectingViewport({
threshold: 1,
})
) {
return;
}
await this.scrollIntoView();
}

async #getOOPIFOffsets(
Expand Down
12 changes: 12 additions & 0 deletions test/src/click.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ describe('Page.click', function () {
await Promise.all([page.click('a'), page.waitForNavigation()]);
expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
});
it('should scroll and click with disabled javascript', async () => {
const {page, server} = getTestState();

await page.setJavaScriptEnabled(false);
await page.goto(server.PREFIX + '/wrappedlink.html');
const body = await page.waitForSelector('body');
await body!.evaluate(el => {
el.style.paddingTop = '3000px';
});
await Promise.all([page.click('a'), page.waitForNavigation()]);
expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
});
it('should click when one of inline box children is outside of viewport', async () => {
const {page} = getTestState();

Expand Down

0 comments on commit a0219fd

Please sign in to comment.