feat(overlays): improve API for overriding controller config in mixin

This commit is contained in:
Joren Broekema 2019-11-26 14:54:33 +01:00 committed by Thomas Allmer
parent 6b2b91f1b3
commit 45f557183d
16 changed files with 173 additions and 87 deletions

View file

@ -40,7 +40,7 @@ The accessibility column indicates whether the functionality is accessible in it
| [icon](./packages/icon) | [![icon](https://img.shields.io/npm/v/@lion/icon.svg)](https://www.npmjs.com/package/@lion/icon) | Display our svg icons | [#173][i173], [#172][i172] |
| [steps](./packages/steps) | [![steps](https://img.shields.io/npm/v/@lion/steps.svg)](https://www.npmjs.com/package/@lion/steps) | Multi Step System | n/a |
| [tabs](./packages/tabs) | [![tBS](https://img.shields.io/npm/v/@lion/tabs.svg)](https://www.npmjs.com/package/@lion/tabs) | Move between a small number of equally important views | n/a |
| **-- Forms --** | | |
| **-- Forms --** | | | |
| [form](./packages/form) | [![form](https://img.shields.io/npm/v/@lion/form.svg)](https://www.npmjs.com/package/@lion/form) | Wrapper for multiple form elements | ✔️ |
| [field](./packages/field) | [![field](https://img.shields.io/npm/v/@lion/field.svg)](https://www.npmjs.com/package/@lion/field) | Base Class for all inputs | [#190][i190] |
| [fieldset](./packages/fieldset) | [![fieldset](https://img.shields.io/npm/v/@lion/fieldset.svg)](https://www.npmjs.com/package/@lion/fieldset) | Group for form inputs | ✔️ |
@ -48,7 +48,7 @@ The accessibility column indicates whether the functionality is accessible in it
| [checkbox](./packages/checkbox) | [![checkbox](https://img.shields.io/npm/v/@lion/checkbox.svg)](https://www.npmjs.com/package/@lion/checkbox) | Checkbox form element | ✔️ |
| [checkbox-group](./packages/checkbox-group) | [![checkbox-group](https://img.shields.io/npm/v/@lion/checkbox-group.svg)](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes | ✔️ |
| [input](./packages/input) | [![input](https://img.shields.io/npm/v/@lion/input.svg)](https://www.npmjs.com/package/@lion/input) | Input element for strings | ✔️ |
| [input-amount](./packages/input-amount) | [![input-amount](https://img.shields.io/npm/v/@lion/input-amount.svg)](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | [#166][i166] | ✔️ |
| [input-amount](./packages/input-amount) | [![input-amount](https://img.shields.io/npm/v/@lion/input-amount.svg)](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | [#166][i166] |
| [input-date](./packages/input-date) | [![input-date](https://img.shields.io/npm/v/@lion/input-date.svg)](https://www.npmjs.com/package/@lion/input-date) | Input element for dates | ✔️ |
| [input-datepicker](./packages/input-datepicker) | [![input-datepicker](https://img.shields.io/npm/v/@lion/input-datepicker.svg)](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
| [input-email](./packages/input-email) | [![input-email](https://img.shields.io/npm/v/@lion/input-email.svg)](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | [#169][i169] |
@ -58,9 +58,9 @@ The accessibility column indicates whether the functionality is accessible in it
| [select](./packages/select) | [![select](https://img.shields.io/npm/v/@lion/select.svg)](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
| [textarea](./packages/textarea) | [![textarea](https://img.shields.io/npm/v/@lion/textarea.svg)](https://www.npmjs.com/package/@lion/textarea) | Multiline text input | [#165][i165] |
| **-- Overlays --** | | | |
| [overlays](./packages/overlays) | [![overlays](https://img.shields.io/npm/v/@lion/overlays.svg)](https://www.npmjs.com/package/@lion/overlays) | Overlays System using lit-html for rendering | ✔️ |
| [popup](./packages/popup) | [![popup](https://img.shields.io/npm/v/@lion/popup.svg)](https://www.npmjs.com/package/@lion/popup) | Popup element | [#175][i175], [#174][i174] |
| [tooltip](./packages/tooltip) | [![tooltip](https://img.shields.io/npm/v/@lion/tooltip.svg)](https://www.npmjs.com/package/@lion/tooltip) | Popup element | [#178][i178], [#177][i177], [#176][i176], [#175][i175], [#174][i174] |
| [overlays](./packages/overlays) | [![overlays](https://img.shields.io/npm/v/@lion/overlays.svg)](https://www.npmjs.com/package/@lion/overlays) | Overlay System | ✔️ |
| [dialog](./packages/dialog) | [![dialog](https://img.shields.io/npm/v/@lion/dialog.svg)](https://www.npmjs.com/package/@lion/dialog) | Dialog element | ✔️ |
| [tooltip](./packages/tooltip) | [![tooltip](https://img.shields.io/npm/v/@lion/tooltip.svg)](https://www.npmjs.com/package/@lion/tooltip) | Tooltip element | [#178][i178], [#177][i177], [#176][i176], [#175][i175], [#174][i174] |
## How to use

View file

@ -108,10 +108,16 @@ describe('<lion-calendar>', () => {
`),
);
expect(
elObj.checkForAllDayObjs(o => o.buttonEl.getAttribute('tabindex') === '0', n => n === 5),
elObj.checkForAllDayObjs(
o => o.buttonEl.getAttribute('tabindex') === '0',
n => n === 5,
),
).to.be.true;
expect(
elObj.checkForAllDayObjs(o => o.buttonEl.getAttribute('tabindex') === '-1', n => n !== 5),
elObj.checkForAllDayObjs(
o => o.buttonEl.getAttribute('tabindex') === '-1',
n => n !== 5,
),
).to.be.true;
});

View file

@ -34,7 +34,7 @@ html`
}}>
This is a dialog
<button
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
>x</button>
<div>
<button slot="invoker">

View file

@ -1,16 +1,17 @@
import { OverlayController, withModalDialogConfig, OverlayMixin } from '@lion/overlays';
import { withModalDialogConfig, OverlayMixin } from '@lion/overlays';
import { LitElement, html } from '@lion/core';
export class LionDialog extends OverlayMixin(LitElement) {
constructor() {
super();
this.closeEventName = 'dialog-close';
}
// eslint-disable-next-line class-methods-use-this
_defineOverlay({ contentNode, invokerNode }) {
return new OverlayController({
_defineOverlayConfig() {
return {
...withModalDialogConfig(),
elementToFocusAfterHide: invokerNode,
contentNode,
invokerNode,
...this.config, // lit-property set by user for overrides
});
};
}
_setupOpenCloseListeners() {
@ -21,12 +22,10 @@ export class LionDialog extends OverlayMixin(LitElement) {
this.opened = !this.opened;
};
this._overlayCtrl.invokerNode.addEventListener('click', this.__toggle);
this._overlayCtrl.contentNode.addEventListener('close', this.__close);
}
_teardownOpenCloseListeners() {
this._overlayCtrl.invokerNode.removeEventListener('click', this.__toggle);
this._overlayCtrl.contentNode.removeEventListener('close', this.__close);
}
render() {

View file

@ -82,7 +82,7 @@ storiesOf('Overlays Specific WC | Dialog', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
></lion-button
>
</div>
@ -98,7 +98,7 @@ storiesOf('Overlays Specific WC | Dialog', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
></lion-button
>
</div>
@ -123,7 +123,7 @@ storiesOf('Overlays Specific WC | Dialog', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
></lion-button
>
</div>

View file

@ -35,7 +35,9 @@ describe('lion-dialog', () => {
<lion-dialog>
<div slot="content" class="dialog">
Hey there
<button @click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}>
<button
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
>
x
</button>
</div>

View file

@ -177,16 +177,28 @@ describe('<lion-fieldset>', () => {
expect(el.modelValue).to.deep.equal({
lastName: 'Bar',
newfieldset: {
'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'football' }],
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: true, value: 'chess' },
{ checked: false, value: 'football' },
],
'gender[]': [
{ checked: false, value: 'male' },
{ checked: false, value: 'female' },
],
color: { checked: false, value: 'blue' },
},
});
el.modelValue = {
lastName: 2,
newfieldset: {
'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'baseball' }],
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: true, value: 'chess' },
{ checked: false, value: 'baseball' },
],
'gender[]': [
{ checked: false, value: 'male' },
{ checked: false, value: 'female' },
],
color: { checked: false, value: 'blue' },
},
};
@ -546,7 +558,10 @@ describe('<lion-fieldset>', () => {
expect(fieldset.formElements['hobbies[]'][0].serializedValue).to.equal('Bar-serialized');
expect(fieldset.serializeGroup()).to.deep.equal({
'hobbies[]': ['Bar-serialized', { checked: false, value: 'rugby' }],
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
'gender[]': [
{ checked: false, value: 'male' },
{ checked: false, value: 'female' },
],
color: { checked: false, value: 'blue' },
});
});
@ -562,15 +577,27 @@ describe('<lion-fieldset>', () => {
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
expect(fieldset.serializeGroup()).to.deep.equal({
'hobbies[]': [{ checked: true, value: 'football' }, { checked: false, value: 'rugby' }],
'gender[]': [{ checked: true, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: true, value: 'football' },
{ checked: false, value: 'rugby' },
],
'gender[]': [
{ checked: true, value: 'male' },
{ checked: false, value: 'female' },
],
color: { checked: false, value: 'blue' },
});
fieldset.formElements.color.disabled = true;
expect(fieldset.serializeGroup()).to.deep.equal({
'hobbies[]': [{ checked: true, value: 'football' }, { checked: false, value: 'rugby' }],
'gender[]': [{ checked: true, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: true, value: 'football' },
{ checked: false, value: 'rugby' },
],
'gender[]': [
{ checked: true, value: 'male' },
{ checked: false, value: 'female' },
],
});
});
@ -594,8 +621,14 @@ describe('<lion-fieldset>', () => {
expect(fieldset.serializeGroup()).to.deep.equal({
comment: 'Foo',
newfieldset: {
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: false, value: 'chess' },
{ checked: false, value: 'rugby' },
],
'gender[]': [
{ checked: false, value: 'male' },
{ checked: false, value: 'female' },
],
color: { checked: false, value: 'blue' },
},
});
@ -622,8 +655,14 @@ describe('<lion-fieldset>', () => {
expect(fieldset.serializeGroup()).to.deep.equal({
comment: 'Foo',
newfieldset: {
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: false, value: 'chess' },
{ checked: false, value: 'rugby' },
],
'gender[]': [
{ checked: false, value: 'male' },
{ checked: false, value: 'female' },
],
},
});
@ -631,8 +670,14 @@ describe('<lion-fieldset>', () => {
expect(fieldset.serializeGroup()).to.deep.equal({
comment: 'Foo',
newfieldset: {
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: false, value: 'chess' },
{ checked: false, value: 'rugby' },
],
'gender[]': [
{ checked: false, value: 'male' },
{ checked: false, value: 'female' },
],
color: { checked: false, value: 'blue' },
},
});
@ -647,8 +692,14 @@ describe('<lion-fieldset>', () => {
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
expect(fieldset.serializeGroup()).to.deep.equal({
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
'hobbies[]': [
{ checked: false, value: 'chess' },
{ checked: false, value: 'rugby' },
],
'gender[]': [
{ checked: false, value: 'male' },
{ checked: false, value: 'female' },
],
color: { checked: false, value: 'blue' },
});
});

View file

@ -1,6 +1,6 @@
import { html, ifDefined, render } from '@lion/core';
import { LionInputDate } from '@lion/input-date';
import { OverlayController, withModalDialogConfig, OverlayMixin } from '@lion/overlays';
import { withModalDialogConfig, OverlayMixin } from '@lion/overlays';
import '@lion/calendar/lion-calendar.js';
import './lion-calendar-overlay-frame.js';
@ -255,17 +255,14 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
/**
* @override Configures OverlayMixin
* @desc returns an instance of a (dynamic) overlay controller
* @returns {OverlayController}
* @desc overrides default configuration options for this component
* @returns {Object}
*/
// eslint-disable-next-line class-methods-use-this
_defineOverlay({ contentNode, invokerNode }) {
const ctrl = new OverlayController({
_defineOverlayConfig() {
return {
...withModalDialogConfig(),
contentNode,
invokerNode,
});
return ctrl;
};
}
async __openCalendarOverlay() {

View file

@ -53,7 +53,7 @@ const template = html`
<button slot="invoker">Click me!</button>
<div slot="content">
<div>Hello, World!</div>
<button @click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}>
<button @click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}>
Close
</button>
</div>

View file

@ -152,7 +152,7 @@ export class OverlayController {
// TODO: Instead, prefetch it or use a preloader-manager to load it during idle time
this.constructor.popperModule = preloadPopper();
}
this.__mergePopperConfigs(this.config.popperConfig || {});
this.__mergePopperConfigs(cfgToAdd.popperConfig || {});
}
this._handleFeatures({ phase: 'init' });
}

View file

@ -1,4 +1,5 @@
import { render, dedupeMixin } from '@lion/core';
import { OverlayController } from './OverlayController.js';
/**
* @type {Function()}
@ -18,12 +19,16 @@ export const OverlayMixin = dedupeMixin(
config: {
type: Object,
},
closeEventName: {
type: String,
},
};
}
constructor() {
super();
this.config = {};
this.closeEventName = 'overlay-close';
}
get opened() {
@ -59,10 +64,29 @@ export const OverlayMixin = dedupeMixin(
/**
* @overridable method `_defineOverlay`
* @desc returns an instance of a (dynamic) overlay controller
* In case overriding _defineOverlayConfig is not enough
* @returns {OverlayController}
*/
// eslint-disable-next-line
_defineOverlay({ contentNode, invokerNode }) {}
_defineOverlay({ contentNode, invokerNode }) {
return new OverlayController({
contentNode,
invokerNode,
...this._defineOverlayConfig(),
...this.config,
});
}
/**
* @overridable method `_defineOverlay`
* @desc returns an object with default configuration options for your overlay component.
* This is generally speaking easier to override than _defineOverlay method entirely.
* @returns {OverlayController}
*/
// eslint-disable-next-line
_defineOverlayConfig() {
return {};
}
/**
* @overridable
@ -84,6 +108,14 @@ export const OverlayMixin = dedupeMixin(
super.connectedCallback();
}
this._createOverlay();
// Default close event catcher on the contentNode which is useful if people want to close
// their overlay but the content is not in the global root node (nowhere near the overlay component)
this.__close = () => {
this.opened = false;
};
this._overlayCtrl.contentNode.addEventListener(this.closeEventName, this.__close);
this._setupOpenCloseListeners();
this.__syncOpened();
this.__syncPopper();
@ -91,9 +123,7 @@ export const OverlayMixin = dedupeMixin(
firstUpdated(c) {
super.firstUpdated(c);
if (this._overlayCtrl.config.placementMode === 'local') {
this._createOutletForLocalOverlay();
}
this._createOutletForLocalOverlay();
}
updated(c) {
@ -107,6 +137,7 @@ export const OverlayMixin = dedupeMixin(
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
this._overlayCtrl.contentNode.removeEventListener(this.closeEventName, this.__close);
this._teardownOpenCloseListeners();
}
@ -114,7 +145,8 @@ export const OverlayMixin = dedupeMixin(
return Array.from(this.children).find(child => child.slot === 'invoker');
}
// FIXME: This should be refactored to Array.from(this.children).find(child => child.slot === 'content')
// FIXME: This should be refactored to
// Array.from(this.children).find(child => child.slot === 'content')
// When this issue is fixed https://github.com/ing-bank/lion/issues/382
get _overlayContentNode() {
const contentNode = this.querySelector('[slot=content]');
@ -184,7 +216,7 @@ export const OverlayMixin = dedupeMixin(
__syncPopper() {
if (this._overlayCtrl) {
// TODO: Use updateConfig directly.. but first check if this sync is even still needed! Maybe we can remove it.
// TODO: Use updateConfig directly.. But maybe we can remove this entirely.
this._overlayCtrl.updatePopperConfig(this.config.popperConfig);
}
}

View file

@ -7,7 +7,6 @@ import {
withDropdownConfig,
withModalDialogConfig,
OverlayMixin,
OverlayController,
} from '../index.js';
function renderOffline(litHtmlTemplate) {
@ -96,14 +95,16 @@ const overlayDemoStyle = css`
customElements.define(
'lion-demo-overlay',
class extends OverlayMixin(LitElement) {
constructor() {
super();
this.closeEventName = 'demo-overlay-close';
}
// eslint-disable-next-line class-methods-use-this
_defineOverlay({ contentNode, invokerNode }) {
return new OverlayController({
_defineOverlayConfig() {
return {
placementMode: 'global', // have to set a default
contentNode,
invokerNode,
...this.config,
});
};
}
_setupOpenCloseListeners() {
@ -114,12 +115,10 @@ customElements.define(
this.opened = !this.opened;
};
this._overlayCtrl.invokerNode.addEventListener('click', this.__toggle);
this._overlayCtrl.contentNode.addEventListener('close', this.__close);
}
_teardownOpenCloseListeners() {
this._overlayCtrl.invokerNode.removeEventListener('click', this.__toggle);
this._overlayCtrl.contentNode.removeEventListener('close', this.__close);
}
render() {
@ -161,7 +160,8 @@ storiesOf('Overlay System | Overlay as a WC', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
>
</div>
@ -179,7 +179,8 @@ storiesOf('Overlay System | Overlay as a WC', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
>
</div>
@ -211,7 +212,8 @@ storiesOf('Overlay System | Overlay as a WC', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
>
</div>
@ -275,7 +277,8 @@ storiesOf('Overlay System | Overlay as a WC', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
>
</div>
@ -410,7 +413,7 @@ storiesOf('Overlay System | Overlay as a WC', module)
Hello! You can close this notification here:
<lion-button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
@click=${e => e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
>
</div>

View file

@ -1,5 +1,5 @@
import { html, css, LitElement, SlotMixin } from '@lion/core';
import { OverlayController, withDropdownConfig, OverlayMixin } from '@lion/overlays';
import { withDropdownConfig, OverlayMixin } from '@lion/overlays';
import { FormControlMixin, InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
import { ValidateMixin } from '@lion/validate';
import './differentKeyNamesShimIE.js';
@ -601,12 +601,10 @@ export class LionSelectRich extends OverlayMixin(
}
// eslint-disable-next-line class-methods-use-this
_defineOverlay({ invokerNode, contentNode } = {}) {
return new OverlayController({
_defineOverlayConfig() {
return {
...withDropdownConfig(),
contentNode,
invokerNode,
});
};
}
__setupOverlay() {

View file

@ -17,7 +17,7 @@ e invoker element is focused.
### Installation
```sh
npm i --save @lion/popup
npm i --save @lion/tooltip
```
```js
@ -28,7 +28,7 @@ import '@lion/tooltip/lion-tooltip.js';
```html
<lion-tooltip>
<div slot="content" class="tooltip">This is a popup<div>
<div slot="content" class="tooltip">This is a tooltip<div>
<a slot="invoker" href="https://www.google.com/">
Popup on link
</a>

View file

@ -33,8 +33,7 @@
],
"dependencies": {
"@lion/core": "^0.3.0",
"@lion/overlays": "^0.6.4",
"@lion/popup": "^0.3.20"
"@lion/overlays": "^0.6.4"
},
"devDependencies": {
"@lion/button": "^0.3.43",

View file

@ -1,21 +1,20 @@
import { OverlayMixin, OverlayController } from '@lion/overlays';
import { OverlayMixin } from '@lion/overlays';
import { LitElement, html } from '@lion/core';
export class LionTooltip extends OverlayMixin(LitElement) {
constructor() {
super();
this.closeEventName = 'tooltip-close';
this.mouseActive = false;
this.keyActive = false;
}
_defineOverlay({ contentNode, invokerNode }) {
return new OverlayController({
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
return {
placementMode: 'local', // have to set a default
elementToFocusAfterHide: null,
contentNode,
invokerNode,
...this.config,
});
};
}
_setupOpenCloseListeners() {