Skip to content

Commit

Permalink
framework: enable popover API (#8192)
Browse files Browse the repository at this point in the history
  • Loading branch information
nnaydenow authored Apr 2, 2024
1 parent 016f237 commit b06c349
Show file tree
Hide file tree
Showing 132 changed files with 1,468 additions and 3,022 deletions.
30 changes: 1 addition & 29 deletions docs/5-development/05-testing-UI5-Web-Components.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,32 +218,4 @@ UI5 Web Components versions up to, including, `1.0.0-rc.15`, used to recommend t
If you have already written tests for your custom UI5 Web Components using the *synchronous* syntax, and you update to a later version than `1.0.0-rc.15`, your tests will no longer run.
You have 2 options:
- Rewrite all tests to use the *asynchronous* syntax. Click the link above to see some examples. This is the **recommended** approach, because the *synchronous* syntax will no longer work with future `nodejs` versions.
- For the time being, adapt your WebdriverIO configuration to continue supporting the *synchronous* syntax.

### 5.1 Supporting the synchronous syntax for writing tests

- Change your `config/wdio.conf.js` file's content from:

```js
module.exports = require("@ui5/webcomponents-tools/components-package/wdio.js");
```
to:

```js
module.exports = require("@ui5/webcomponents-tools/components-package/wdio.sync.js");
```

This will give you the exact same WebdriverIO configuration, but with *synchronous* custom commands (such as `getProperty`, `setProperty`, `hasClass`, etc.).

- Manually install `@wdio/sync`

You can install it with `npm`:

`npm i --save-dev @wdio/sync`

or with `yarn`:

`yarn add -D @wdio/sync`

Just installing the package (with no extra configuration) is enough to let WebdriverIO run the *synchronous* tests.

- For the time being, adapt your WebdriverIO configuration to continue supporting the *synchronous* syntax.
316 changes: 0 additions & 316 deletions docs/5-development/06-deep-dive-and-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ as this article will expand on many of the notions, introduced there.
- [`onBeforeRendering`](#lifecycle_before)
- [`onAfterRendering`](#lifecycle_after)
- [`onEnterDOM` and `onExitDOM`](#lifecycle_dom)
4. [The static area](#static)
- [Preface](#static_preface)
- [What is the static area and why is it needed?](#static_what_why)
- [Using the static area?](#static_using)
- [Accessing the static area item](#static_accessing)

## Metadata deep dive <a name="metadata"></a>

Expand Down Expand Up @@ -1086,314 +1081,3 @@ and then in `onEnterDOM` and `onExitDOM` we register/deregister this function wi

Then, whenever the component resizes, the `ResizeHandler` will trigger the callback, the metadata `_width` property will be updated to a new value in `_onResize`,
the component will be invalidated, and the template will be executed with the new value of `_width`, respectively `styles`.

## The static area <a name="static"></a>

### Preface <a name="static_preface"></a>

This section expands on the UI5 Web Components class structure, so if you haven't, please check [Developing Custom UI5 Web Components](./02-custom-UI5-Web-Components.md) first.

Normally, the whole HTML markup of a UI5 Web Component is found in one place - the shadow DOM of the custom element itself.

Example:

```html
<ui5-button id="button">Click me</ui5-button>
```

All HTML, belonging to this `ui5-button` instance is in its own shadow DOM.

Respectively, in the class where the button component is defined, we provide one template and one piece of CSS:

```js
import ButtonTemplate from "./generated/templates/ButtonTemplate.lit.js";
import buttonCss from "./generated/themes/Button.css.js";

class Button extends UI5Element {
...

static get styles() {
return buttonCss;
}

static get template() {
return ButtonTemplate;
}

}
```

These are respectively the template and CSS that are going to be used in the component's shadow DOM.

However, there are more complex components, whose HTML is split in two parts - the custom element's shadow DOM (as is the case with the button),
but also a so called **static area** part, holding all popups this component might open. This is the case with most components that have any kind of
popup-related functionality (dropdowns, rich tooltips, popovers, dialogs). Prominent examples are `ui5-select`, `ui5-combobox`, `ui5-textarea`, `ui5-date-picker`.

### What is the static area and why is it needed? <a name="static_what_why"></a>

The static area is a special *singleton* custom element (`ui5-static-area`), placed automatically by the framework as the first child of the `body`.
For each component, having a **static area** part, a `ui5-static-area-item` custom element is created inside the static area.

```html
<body>
<ui5-static-area> <!-- created automatically only once -->
<ui5-static-area-item></ui5-static-area-item> <!-- created automatically for the ui5-select -->
<ui5-static-area-item></ui5-static-area-item> <!-- created automatically for the ui5-date-picker -->
</ui5-static-area>

<ui5-select></ui5-select> <!-- needs a static area part -->
<ui5-date-picker></ui5-date-picker> <!-- needs a static area part -->
<ui5-button></ui5-button> <!-- does not need a static area part -->
</body>
```

In this example 3 UI5 Web Components are used: `ui5-select`, `ui5-date-picker`, and `ui5-button`.
Since two of them have static area parts, the framework has created a `ui5-static-area` (one for the whole page) and inside it a `ui5-static-area-item`
for each component with a static area part.

Thus, the HTML, defining the `ui5-select` and `ui5-date-picker` components is split in two parts of the HTML page:
- the shadow DOM of the custom element itself (`ui5-select`, `ui5-date-picker`)
- the shadow DOM of the `static-area-item`, created for the respective component.

**This is necessary because such a split is the only way to guarantee that a popup (dropdown, rich tooltip, popover, etc.) will always be
positioned correctly on the HTML page**, even if parts of the page have:
- `transform: translate`
- `overflow: hidden`
- `z-index`

Since the `ui5-statia-area` is a top-level `body` child, it is guaranteed to be on top of everything else on the page with the correct CSS styles,
regardless of the page structure and [stacking context](https://developer.mozilla.org/en-US/docs/Glossary/Stacking_context).

If we did not use a static area, for example as in a component, defined like this:

In the `MySelect.js` file:

```handlebars
<div class="my-select">
<h1>Click to open the dropdown:</h1>
<button @click="{{onOpenDropdownClick}}">Dropdown</button>
<ui5-popover id="#popover" ?open="{{dropdownOpen}}">
<ui5-list>
{{#each dropdownItems}}
<ui5-li>{{text}}</ui5-li>
{{/each}}
</ui5-list>
</ui5-popover>
</div>
```

In the `MySelect.js` file:

```js
class MySelect extends UI5Element {
...
onOpenDropdownClick(event) {
this.dropdownOpen = true;
}
}
```

then when the user clicks the `button`, and the `ui5-popover` opens (due to its `open` property having been set to `true`),
this popover might be partially or entirely "cut" or misplaced, depending on the position of the component on the page.

Example 1:

```html
<body>
<my-select></my-select>
</body>
```

Here the `my-select` component would work just fine as it is the only component on the page and no other components create a stacking context or overflow.

However, consider example 2:

```html
<body>
<div style="height: 20px; overflow: hidden;">
<my-select></my-select>
</div>
</body>
```

Now, when the popover opens, only a `20px`-high strip of it would be visible due to the parent element's CSS.

This is an oversimplified example that could easily be fixed, but in real-world scenarios there are often parts of the HTML page we cannot
influence which cause problems with popups.

### Using the static area <a name="static_using"></a>

Here is how we can rework the component from the example above to take advantage of the static area:

1. Split the template and CSS of the component:

Instead of having the dropdown (`ui5-popover`) in the main template:

```handlebars
<div class="my-select">
<h1>Click to open the dropdown:</h1>
<button @click="{{onOpenDropdownClick}}">Dropdown</button>
<ui5-popover id="#popover" ?open="{{dropdownOpen}}">
<ui5-list>
{{#each dropdownItems}}
<ui5-li>{{text}}</ui5-li>
{{/each}}
</ui5-list>
</ui5-popover>
</div>
```

split `MySelect.hbs` into `MySelect.hbs` and `MySelectDropdown.hbs`:

The `MySelect.hbs` file:

```handlebars
<div class="my-select">
<h1>Click to open the dropdown:</h1>
<button @click="{{onOpenDropdownClick}}">Dropdown</button>
</div>
```

The `MySelectDropdown.hbs` file:

```handlebars
<ui5-popover id="#popover" ?open="{{dropdownOpen}}">
<ui5-list>
{{#each dropdownItems}}
<ui5-li>{{text}}</ui5-li>
{{/each}}
</ui5-list>
</ui5-popover>
```

Also, create the CSS of the component in 2 files:
- `MySelect.css` (with styles for the select itself, f.e. `.my-select {}`)
- `MySelectDropdown.css` (with styles for the dropdown only, f.e. `#dropdown {}`)

2. Pass the new template and CSS to the component class

The `MySelect.js` file:

```js
import MySelectTemplate from "./generated/templates/MySelect.lit.js";
import MySelectDropdownTemplate from "./generated/templates/MySelectDropdown.lit.js";

import mySelectCss from "./generated/themes/MySelect.css.js";
import mySelectDropdownCss from "./generated/themes/MySelectDropdown.css.js";

class MySelect extends UI5Element {
...

static get styles() {
return mySelectCss;
}

static get staticAreaStyles() {
return mySelectDropdownCss;
}

static get template() {
return MySelectTemplate;
}

static get staticAreaTemplate() {
return MySelectDropdownTemplate;
}

}
```

Creating the `static get staticAreaTemplate()` method is the indication that your component has a static area part,
and will trigger the respective framework functionality to support it.

3. Use the `async getStaticAreaItemDomRef()` method to create the static area item **on demand**, whenever necessary.

```js
class MySelect extends UI5Element {
...

async onOpenDropdownClick() {
await this.getStaticAreaItemDomRef(); // this line is new compared to the old implementation
this.dropdownOpen = true;
}

}
```

This is all it takes to make your component work with the static area.

**Important:** please note that the static area item is only created **on demand** - when you call the `async getStaticAreaItemDomRef()` function.
For most components this is when the user opens a menu/dropdown/hovers over an element for a tooltip, etc.

Let's go over the whole process in more detail:

1. The browser renders a `<my-select></my-select>` component:

```html
<body>
<my-select></my-select>
</body>
```

The shadow root of the `my-select` component will be created with the content from the `MySelect.hbs` template, as it was provided as `static get template()`.
Note that until this point nothing related to the static area has happened. The lifecycle of this component so far is not much different than that of a `ui5-button`.

2. The user interacts with the component (clicks the "Dropdown" button)

This will trigger the `onOpenDropdownClick` event handler we've bound in `MySelect.hbs`
and once the first line of this event handler is executed (the `await this.getStatiAreaItemDomRef` part):

```js
async onOpenDropdownClick() {
await this.getStaticAreaItemDomRef();
this.dropdownOpen = true;
}
```

the framework will create the `ui5-static-area` and a `ui5-static-area-item` and will create its shadow root with the content from the `MySelectDropdown.hbs` template, as it was provided as `static get staticAreaTemplate()`.

The DOM would then look like this:

```html
<body>
<ui5-static-area>
<ui5-static-area-item>
#shadow-root <!-- The MySelectDropdown.hbs template was rendered here -->
</ui5-static-area-item>
</ui5-static-area>

<my-select>
#shadow-root <!-- The MySelect.hbs template was rendered here -->
</my-select>
</body>
```

If the user hadn't clicked the button, the static area part would not have been created at all.

### Accessing the static area item <a name="static_accessing"></a>

The `async getStaticAreaItemDomRef()` function from the example above:

```js
async onOpenDropdownClick() {
await this.getStaticAreaItemDomRef();
this.dropdownOpen = true;
}
```

returns a reference to the `shadowRoot` of the static area item for this component.

You can therefore access it like this:

```js
const staticAreaItem = await this.getStaticAreaItemDomRef();
const popover = staticAreaItem.querySelector("[ui5-popover]");
```

First, we get a reference to the static area item's shadow root in `staticAreaItem`, and then we get the instance of the `ui5-popover` element
by using the attribute selector (`[ui5-popover]`), as is the best practice. See [Tag](#metadata_tag) in the [Metadata deep dive](#metadata) section above.

Also, note that no matter how many times you call `getStaticAreaItemDomRef`, the static area item will be created only the first time.

5 changes: 2 additions & 3 deletions docs/5-development/07-typescript-in-UI5-Web-Components.md
Original file line number Diff line number Diff line change
Expand Up @@ -677,9 +677,8 @@ It's important to note that casting the returned result will exclude "`null`." A


```ts
async _getDialog() {
const staticAreaItem = await this.getStaticAreaItemDomRef();
return staticAreaItem!.querySelector<Dialog>("[ui5-dialog]")!;
_getDialog() {
return this.shadowRoot!.querySelector<Dialog>("[ui5-dialog]")!;
}
```

Expand Down
1 change: 0 additions & 1 deletion packages/base/bundle.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import "./test/elements/Generic.js";
import "./test/elements/NoShadowDOM.js";
import "./test/elements/Parent.js";
import "./test/elements/Child.js";
import "./test/elements/WithStaticArea.js";
import "./test/elements/WithComplexTemplate.js";
import "./test/elements/GenericExt.js";

Expand Down
Loading

0 comments on commit b06c349

Please sign in to comment.