Skip to content

Commit

Permalink
Improve widget example
Browse files Browse the repository at this point in the history
Add listeners
  • Loading branch information
fcollonval committed Nov 13, 2023
1 parent cf46e13 commit 6959868
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 10 deletions.
73 changes: 64 additions & 9 deletions widgets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The base widget class can be imported with:
```ts
// src/index.ts#L8-L8

import { Widget } from '@lumino/widgets';
import { Message } from '@lumino/messaging';
```

It requires to add the library as package dependency:
Expand All @@ -36,7 +36,7 @@ of the `app` object:
```ts
// src/index.ts#L19-L19

const { commands, shell } = app;
requires: [ICommandPalette],
```

Then the widget can be inserted by calling the `add` method, like in the command defined
Expand All @@ -46,10 +46,10 @@ in this example:
```ts
// src/index.ts#L25-L28

label: 'Open a Tab Widget',
caption: 'Open the Widgets Example Tab',
execute: () => {
const widget = new ExampleWidget();
shell.add(widget, 'main');
}
```
<!-- prettier-ignore-end -->
Expand All @@ -62,33 +62,88 @@ In this case, no specific behavior is defined for the widget. Only some properti
- `title.label`: The widget tab title
- `title.closable`: Allow the widget tab to be closed
<!-- prettier-ignore-start -->
```ts
// src/index.ts#L36-L44
// src/index.ts#L36-L43

export default extension;

class ExampleWidget extends Widget {
constructor() {
super();
this.addClass('jp-example-view');
this.id = 'simple-widget-example';
this.title.label = 'Widget Example View';
this.title.closable = true;
}
}
```
<!-- prettier-ignore-end -->
You can associate style properties to the custom CSS class in the file
`style/base.css`:
<!-- prettier-ignore-start -->
<!-- embedme style/base.css#L7-L9 -->
<!-- embedme style/base.css#L7-L10 -->
```css
.jp-example-view {
background-color: aliceblue;
cursor: pointer;
}
```
<!-- prettier-ignore-end -->
## Adding event listeners
A very often required need for widgets is the ability to react to user events.
As widget is a wrapper around a HTML element accessible through the attribute
`this.node`, you can add event listeners using the standard API:
```ts
// src/index.ts#L69-L75

// The first two events are not linked to a specific callback but
// to this object. In that case, the object method `handleEvent`
// is the function called when an event occurs.
this.node.addEventListener('pointerenter', this);
this.node.addEventListener('pointerleave', this);
// This event will call a specific function when occuring
this.node.addEventListener('click', this._onEventClick.bind(this));
```
The listeners can either be directly a function as for the _click_ event in this
example or the widget (as for _pointerenter_ and _pointerleave_ here). In the
second case, you will need to defined a `handleEvent` method in the widget that will
be called when an event is triggered:
```ts
// src/index.ts#L52-L61

handleEvent(event: Event): void {
switch (event.type) {
case 'pointerenter':
this._onMouseEnter(event);
break;
case 'pointerleave':
this._onMouseLeave(event);
break;
}
}
```
The best place for adding listeners is the method `onAfterAttach` that is inherited
by the `Widget` class and is called when the widget is attached to the DOM. And you
should remove the listeners in `onBeforeDetach` when the widget is about to be detached
from the DOM.
```ts
// src/index.ts#L83-L87

protected onBeforeDetach(msg: Message): void {
this.node.removeEventListener('pointerenter', this);
this.node.removeEventListener('pointerleave', this);
this.node.removeEventListener('click', this._onEventClick.bind(this));
}
```
## Where to Go Next
This example uses a command to display the widget. Have a look a the
Expand Down
65 changes: 65 additions & 0 deletions widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {

import { ICommandPalette } from '@jupyterlab/apputils';

import { Message } from '@lumino/messaging';

import { Widget } from '@lumino/widgets';

/**
Expand Down Expand Up @@ -41,4 +43,67 @@ class ExampleWidget extends Widget {
this.title.label = 'Widget Example View';
this.title.closable = true;
}

/**
* Event generic callback on an object as defined in the specification
*
* See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#the_event_listener_callback
*/
handleEvent(event: Event): void {
switch (event.type) {
case 'pointerenter':
this._onMouseEnter(event);
break;
case 'pointerleave':
this._onMouseLeave(event);
break;
}
}

/**
* Callback when the widget is added to the DOM
*
* This is the recommended place to listen for DOM events
*/
protected onAfterAttach(msg: Message): void {
// The first two events are not linked to a specific callback but
// to this object. In that case, the object method `handleEvent`
// is the function called when an event occurs.
this.node.addEventListener('pointerenter', this);
this.node.addEventListener('pointerleave', this);
// This event will call a specific function when occuring
this.node.addEventListener('click', this._onEventClick.bind(this));
}

/**
* Callback when the widget is removed from the DOM
*
* This is the recommended place to stop listening for DOM events
*/
protected onBeforeDetach(msg: Message): void {
this.node.removeEventListener('pointerenter', this);
this.node.removeEventListener('pointerleave', this);
this.node.removeEventListener('click', this._onEventClick.bind(this));
}

/**
* Callback on click on the widget
*/
private _onEventClick(event: Event): void {
window.alert('You clicked on the widget');
}

/**
* Callback on pointer entering the widget
*/
private _onMouseEnter(event: Event): void {
this.node.style['backgroundColor'] = 'orange';
}

/**
* Callback on pointer leaving the widget
*/
private _onMouseLeave(event: Event): void {
this.node.style['backgroundColor'] = 'aliceblue';
}
}
1 change: 1 addition & 0 deletions widgets/style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

.jp-example-view {
background-color: aliceblue;
cursor: pointer;
}
10 changes: 9 additions & 1 deletion widgets/ui-tests/tests/widgets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ test('should open a widget panel', async ({ page }) => {
// Open a new tab from menu
await page.menu.clickMenuItem('Widget Example>Open a Tab Widget');

await page.click('div[role="main"] >> text=Widget Example View');
await page.getByRole('main').getByText('Widget Example View');

let gotAlerted = false;
page.on('dialog', dialog => {
gotAlerted = dialog.message() == 'You clicked on the widget';
});
await page.getByRole('main').locator('.jp-example-view').click();

Check failure on line 19 in widgets/ui-tests/tests/widgets.spec.ts

View workflow job for this annotation

GitHub Actions / build_extensions (widgets, ubuntu-latest)

tests/widgets.spec.ts:3:5 › should open a widget panel

1) tests/widgets.spec.ts:3:5 › should open a widget panel ──────────────────────────────────────── Error: locator.click: Target closed =========================== logs =========================== waiting for getByRole('main').locator('.jp-example-view') locator resolved to <div role="tabpanel" id="simple-widget-example" aria-…></div> attempting click action waiting for element to be visible, enabled and stable element is visible, enabled and stable scrolling into view if needed done scrolling performing click action ============================================================ 17 | gotAlerted = dialog.message() == 'You clicked on the widget'; 18 | }); > 19 | await page.getByRole('main').locator('.jp-example-view').click(); | ^ 20 | 21 | expect(gotAlerted).toEqual(true); 22 | at /home/runner/work/extension-examples/extension-examples/widgets/ui-tests/tests/widgets.spec.ts:19:60

expect(gotAlerted).toEqual(true);

expect(await page.screenshot()).toMatchSnapshot('widgets-example.png');
});

0 comments on commit 6959868

Please sign in to comment.