Skip to main content

Widgets

Widgets provide a way for plugins to develop and integrate customized user interfaces. They are additional iframes which can be added via the plugin.

Up until now, to add UI elements, a plugin needed to ask Web App 3 to do it on its behalf. This helps with delegating responsibility and assuring the plugin adheres to the established design principles.

One major downside is that UI becomes very reliant on Web App 3's versions and subsequently release cycle. The current capabilities make building a simple plugin quite easy, and we are not taking this away, however the need to offer support for use cases with more complex UI is there.

Adding a widget

A Widget is added by calling the plugin.ui.addWidget endpoint. It requires that the id of the plugin in the branding manifest matches the id passed in registerPlugin.

The widget inherits the sandbox security of its parent plugin.

Currently, the widget is loaded when the in-meeting interface of Web App 3 loads. This means widgets are not available for any pre- or post-meeting stages.

The currently supported types are sidePanel and floating widgets. While the options a widget supports depend on its type, there is some common ground.

{
type: 'sidePanel' | 'floating';
src: string;
title: string;
avoidAutoToggle?: {
headerCloseButton?: boolean;
};
}
  • src: This can load an internal or an external resource.
  • title: Every widget comes with a header when loaded into the UI. The title sets the text on that header.
  • avoidAutoToggle: An object of available toggle triggers, which could be adjusted to remove their default behavior to toggle the widget.

Return value

addWidget returns an object with the following common interface

    id: string;
onLoad: Signal<undefined>;
onToggle: Signal<{
widgetId: string;
isVisible: boolean;
isRequest: boolean;
trigger: 'anotherSidePanel' | 'headerCloseButton' | 'self';
}>;
toggle: () => Promise<void>;
remove: () => Promise<void>;
  • id: The id of the widget that is auto-generated by Web App 3
  • onLoad: Signals when the widget is loaded.
  • onToggle: Signals when the widget is toggled. A widget can be toggled by itself by calling its toggle function, or it can be toggled by something external. Some external triggers can be prohibited from auto toggling via the avoidAutoToggle option. If that's the case, isRequest will be true.
  • toggle: Toggles the widget. Please note this only toggles the visibility of the widget. The code behind the widget is loaded only once
  • remove: Removes the widget

SidePanel Widget

The side panel of Web App 3 is where the Chat and Participant panels live. There can be multiple side panel widgets added by an arbitrary number of plugins, however, only 1 can be displayed at a time. Which one, if any, is displayed depends on the sequence of toggle requests made by the various side panel widgets.

Floating Widget

A floating widget is not confined in a pre-defined slot. It can have variable dimensions and a dynamic position. Floating widgets have the following additional options available to them.

{
draggable?: boolean;
isVisible?: boolean;
position?: FloatingWidgetPosition;
dimensions?: FloatingWidgetDimensions;
}
  • draggable: Indicates if the widget should be draggable. Leaving this unspecified defaults to true.

  • isVisible: Should the widget be visible when it loads initially. Leaving this unspecified defaults to false

  • position: Initial widget position with the following available options

    | 'topLeft'
    | 'bottomLeft'
    | 'center'
    | 'topRight'
    | 'bottomRight';

    Defaults to topLeft

  • dimensions: Width and height dimensions. Values must fully specify the value of the width and height properties.

    A Dimension can be set as a string which is fixed for the widget's lifecycle.

    A dimensions can also be set as an object describing a breakpoint-based relationships.

    Supported breakpoints are export const breakpoints = {xs: 0, sm: 479, md: 744, lg: 1025, xl: 2000}; In addition, 'mobilePortrait' | 'mobileLandscape' are also supported to match the rest of the Web App 3 design. They take precedence if another breakpoint is also active.

    A breakpoint dimension is picked if the active breakpoint is less than it. If two or more breakpoint dimensions exist, the one closer to the breakpoint is picked. For example, if the active breakpoint is lg, with a setting of width: {sm: '50%', md: '30%'}, the width will be 30%

    If both width and height have a value of 100%, a special case is triggered where the widget goes fullscreen and is not draggable. The look and feel of this is similar to what the chat and participants panels look on mobile portrait.

    You can have one dimension being set with a fixed value and the other one being breakpoint-based.

Loading an external resource

The following example will add a button to the toolbar which will toggle between showing and hiding Pexip in the side panel.

const widget = await plugin.ui.addWidget({
src: 'https://pexip.com',
type: 'sidePanel',
title: 'Test',
});

const button = await plugin.ui.addButton({
position: 'toolbar',
icon: 'IconPexipLogo',
tooltip: 'Pexip',
});

button.onClick.add(() => {
void widget.toggle();
});

Loading an internal resource

There are several ways to load an internal resource. We think the approach that will provide the best context separation between widgets and the plugin is achieved via vite.config.js.

Let's take the example plugin as a case study (plugin-example.zip) and assume we have an external service that feeds in customer service requests that we want to display in a widget.

The file structure might look something like this:

    |-- src
index.ts
requests.ts
index.html
requests.html
...rest of the files in the plugin-example

index.ts will be your main main entry point that holds the UI to toggle the widget

requests.ts will hold the widget code. For this to be ran, it needs to be included in requests.html

<!doctype html>
<script src="./src/requests.ts" type="module"></script>

To match this file structure the vite.config.js will have to be like so:

export default {
base: './',
build: {
target: 'esnext',
rollupOptions: {
input: ['./index.html', './requests.html'],
output: {
entryFileNames: `assets/[name].js`,
},
},
},
};

The example code in index.ts will then look like this:

const widget = await plugin.ui.addWidget({
src: '${PATH-TO-YOUR-PLUGIN}/custom.html',
type: 'sidePanel',
title: 'Test',
});

const button = await plugin.ui.addButton({
position: 'toolbar',
icon: 'IconPexipLogo',
tooltip: 'Pexip',
});

button.onClick.add(() => {
void widget.toggle();
});

Note that PATH-TO-YOUR-PLUGIN will be the same as the one in your branding manifest. If your plugin config in the manifest is "plugins": [{"src": "./plugins/example-plugin/index.html"}], the source of your widget will be ./plugins/example-plugin/custom.html

Accessing the Plugin API

Widgets can register themselves with the API just like their parent plugins.

const widget = await registerWidget({parentPluginId: 'example-plugin'});

For the registration to be successful, parentPluginId must match the id of the plugin in registerPlugin.

For our example to work, the plugin must have been registered like so: const plugin = await registerPlugin({id: 'example-plugin', version: 0});