From 69598681aa824ce9d2281df0fd6148a090aff84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Mon, 13 Nov 2023 17:57:11 +0100 Subject: [PATCH] Improve widget example Add listeners --- widgets/README.md | 73 ++++++++++++++++++++++---- widgets/src/index.ts | 65 +++++++++++++++++++++++ widgets/style/base.css | 1 + widgets/ui-tests/tests/widgets.spec.ts | 10 +++- 4 files changed, 139 insertions(+), 10 deletions(-) diff --git a/widgets/README.md b/widgets/README.md index 7eb71a8d..e9d9ce1d 100644 --- a/widgets/README.md +++ b/widgets/README.md @@ -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: @@ -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 @@ -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'); -} ``` @@ -62,8 +62,11 @@ 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 + ```ts -// src/index.ts#L36-L44 +// src/index.ts#L36-L43 + +export default extension; class ExampleWidget extends Widget { constructor() { @@ -71,24 +74,76 @@ class ExampleWidget extends Widget { this.addClass('jp-example-view'); this.id = 'simple-widget-example'; this.title.label = 'Widget Example View'; - this.title.closable = true; - } -} ``` + You can associate style properties to the custom CSS class in the file `style/base.css`: - + ```css .jp-example-view { background-color: aliceblue; + cursor: pointer; } ``` +## 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 diff --git a/widgets/src/index.ts b/widgets/src/index.ts index f173dadb..4e9d01ad 100644 --- a/widgets/src/index.ts +++ b/widgets/src/index.ts @@ -5,6 +5,8 @@ import { import { ICommandPalette } from '@jupyterlab/apputils'; +import { Message } from '@lumino/messaging'; + import { Widget } from '@lumino/widgets'; /** @@ -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'; + } } diff --git a/widgets/style/base.css b/widgets/style/base.css index ed27ec0b..177c164b 100644 --- a/widgets/style/base.css +++ b/widgets/style/base.css @@ -6,4 +6,5 @@ .jp-example-view { background-color: aliceblue; + cursor: pointer; } diff --git a/widgets/ui-tests/tests/widgets.spec.ts b/widgets/ui-tests/tests/widgets.spec.ts index fd6388db..6d59ccb6 100644 --- a/widgets/ui-tests/tests/widgets.spec.ts +++ b/widgets/ui-tests/tests/widgets.spec.ts @@ -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(); + + expect(gotAlerted).toEqual(true); expect(await page.screenshot()).toMatchSnapshot('widgets-example.png'); });