Skip to content

Commit

Permalink
feat(workbench/popup): support returning result on focus loss
Browse files Browse the repository at this point in the history
A result can be set on the popup handle that will be returned to the popup opener on focus loss.

```ts
import {inject} from '@angular/core';
import {Popup} from '@scion/workbench';

inject(Popup).setResult('result');
```

BREAKING CHANGE: The method `closeWithError` has been removed from the `Popup` handle. Instead, pass an `Error` object to the `close` method.

**Before migration:**
```ts
import {inject} from '@angular/core';
import {Popup} from '@scion/workbench';

inject(Popup).closeWithError('some error');
```

**After migration:**
```ts
import {inject} from '@angular/core';
import {Popup} from '@scion/workbench';

inject(Popup).close(new Error('some error'));
```
  • Loading branch information
k-genov authored and danielwiehl committed Sep 11, 2024
1 parent 9bfdf74 commit ce5089e
Show file tree
Hide file tree
Showing 23 changed files with 443 additions and 208 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,16 @@
</ng-template>
<ng-template #panel_return_value>
<input [formControl]="form.controls.result" class="e2e-return-value" placeholder="Optional data to return to the popup opener">
<button (click)="onApplyReturnValue()" class="e2e-apply-return-value">Apply</button>
</ng-template>
</sci-accordion>
</form>
</sci-viewport>

<div class="buttons">
<label class="close-with-error">
<sci-checkbox [formControl]="form.controls.closeWithError" class="e2e-close-with-error"/>
Close with error
</label>
<button (click)="onClose()" class="e2e-close" sci-primary>Close</button>
<button (click)="onCloseWithError()" class="e2e-close-with-error">Close (with error)</button>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,16 @@

> div.buttons {
flex: none;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: .25em;
margin-top: 1em;
display: flex;
gap: 2em;
padding-top: 1em;
align-items: center;
justify-content: space-between;

> label.close-with-error {
display: flex;
align-items: center;
gap: .5em;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {A11yModule} from '@angular/cdk/a11y';
import {SciKeyValueComponent} from '@scion/components.internal/key-value';
import {SciFormFieldComponent} from '@scion/components.internal/form-field';
import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/components.internal/accordion';
import {SciCheckboxComponent} from '@scion/components.internal/checkbox';

/**
* Popup test component which can grow and shrink.
Expand All @@ -43,6 +44,7 @@ import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/component
SciAccordionItemDirective,
SciKeyValueComponent,
ReactiveFormsModule,
SciCheckboxComponent,
],
})
export default class PopupPageComponent {
Expand Down Expand Up @@ -91,6 +93,7 @@ export default class PopupPageComponent {
minWidth: this._formBuilder.control('100vw'),
width: this._formBuilder.control(''),
maxWidth: this._formBuilder.control(''),
closeWithError: this._formBuilder.control(false),
result: this._formBuilder.control(''),
});

Expand All @@ -107,11 +110,12 @@ export default class PopupPageComponent {
popup.signalReady();
}

public onClose(): void {
this.popup.close(this.form.controls.result.value);
protected onApplyReturnValue(): void {
this.popup.setResult(this.form.controls.result.value);
}

public onCloseWithError(): void {
this.popup.closeWithError(this.form.controls.result.value);
protected onClose(): void {
const result = this.form.controls.closeWithError.value ? new Error(this.form.controls.result.value) : this.form.controls.result.value;
this.popup.close(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,16 @@
</ng-template>
<ng-template #panel_return_value>
<input [formControl]="form.controls.result" class="e2e-return-value" placeholder="Optional data to return to the popup opener">
<button (click)="onApplyReturnValue()" class="e2e-apply-return-value">Apply</button>
</ng-template>
</sci-accordion>
</form>
</sci-viewport>

<div class="buttons">
<button (click)="onClose()" class="e2e-close">Close</button>
<button (click)="onCloseWithError()" class="e2e-close-with-error">Close (with error)</button>
<label class="close-with-error">
<sci-checkbox [formControl]="form.controls.closeWithError" class="e2e-close-with-error"/>
Close with error
</label>
<button (click)="onClose()" class="e2e-close" sci-primary>Close</button>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,16 @@

> div.buttons {
flex: none;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: .25em;
margin-top: 1em;
display: flex;
gap: 2em;
padding-top: 1em;
align-items: center;
justify-content: space-between;

> label.close-with-error {
display: flex;
align-items: center;
gap: .5em;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {NullIfEmptyPipe} from '../common/null-if-empty.pipe';
import {SciKeyValueComponent} from '@scion/components.internal/key-value';
import {SciFormFieldComponent} from '@scion/components.internal/form-field';
import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/components.internal/accordion';
import {SciCheckboxComponent} from '@scion/components.internal/checkbox';

/**
* Popup component provided by the host app via a popup capability.
Expand All @@ -41,6 +42,7 @@ import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/component
SciAccordionItemDirective,
SciKeyValueComponent,
ReactiveFormsModule,
SciCheckboxComponent,
],
})
export default class HostPopupPageComponent {
Expand Down Expand Up @@ -84,6 +86,7 @@ export default class HostPopupPageComponent {
minWidth: this._formBuilder.control(''),
width: this._formBuilder.control(''),
maxWidth: this._formBuilder.control(''),
closeWithError: this._formBuilder.control(false),
result: this._formBuilder.control(''),
});

Expand All @@ -92,11 +95,12 @@ export default class HostPopupPageComponent {
private _formBuilder: NonNullableFormBuilder) {
}

public onClose(): void {
this.popup.close(this.form.controls.result.value);
protected onApplyReturnValue(): void {
this.popup.setResult(this.form.controls.result.value);
}

public onCloseWithError(): void {
this.popup.closeWithError(this.form.controls.result.value);
protected onClose(): void {
const result = this.form.controls.closeWithError.value ? new Error(this.form.controls.result.value) : this.form.controls.result.value;
this.popup.close(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,16 @@
</ng-template>
<ng-template #panel_return_value>
<input class="e2e-return-value" [formControl]="form.controls.result" placeholder="Optional data to return to the popup opener">
<button (click)="onApplyReturnValue()" class="e2e-apply-return-value">Apply</button>
</ng-template>
</sci-accordion>
</form>
</sci-viewport>

<div class="buttons">
<label class="close-with-error">
<sci-checkbox [formControl]="form.controls.closeWithError" class="e2e-close-with-error"/>
Close with error
</label>
<button (click)="onClose()" class="e2e-close" sci-primary>Close</button>
<button (click)="onCloseWithError()" class="e2e-close-with-error">Close (with error)</button>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@

> div.buttons {
flex: none;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: .25em;
margin-top: 1em;
display: flex;
gap: 1em;
padding-top: 1em;
align-items: center;
justify-content: space-between;

> label.close-with-error {
display: flex;
align-items: center;
gap: .5em;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {NullIfEmptyPipe} from '../common/null-if-empty.pipe';
import {JsonPipe} from '@angular/common';
import {SciFormFieldComponent} from '@scion/components.internal/form-field';
import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/components.internal/accordion';
import {SciCheckboxComponent} from '@scion/components.internal/checkbox';

@Component({
selector: 'app-popup-page',
Expand All @@ -32,6 +33,7 @@ import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/component
SciAccordionComponent,
SciAccordionItemDirective,
ReactiveFormsModule,
SciCheckboxComponent,
],
})
export class PopupPageComponent {
Expand Down Expand Up @@ -75,17 +77,19 @@ export class PopupPageComponent {
minWidth: this._formBuilder.control(''),
width: this._formBuilder.control(''),
maxWidth: this._formBuilder.control(''),
closeWithError: this._formBuilder.control(false),
result: this._formBuilder.control(''),
});

constructor(public popup: Popup, private _formBuilder: NonNullableFormBuilder) {
}

public onClose(): void {
this.popup.close(this.form.controls.result.value);
protected onApplyReturnValue(): void {
this.popup.setResult(this.form.controls.result.value);
}

public onCloseWithError(): void {
this.popup.closeWithError(this.form.controls.result.value);
protected onClose(): void {
const result = this.form.controls.closeWithError.value ? new Error(this.form.controls.result.value) : this.form.controls.result.value;
this.popup.close(result);
}
}
2 changes: 1 addition & 1 deletion docs/site/howto/how-to-open-popup.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const result = await popupService.open({
});
```

To interact with the popup in the popup component, inject the popup handle `Popup`, e.g., to close the popup or read input passed to the popup by the popup opener.
To interact with the popup in the popup component, inject the popup handle `Popup`, e.g., to read input passed to the popup or to close the popup, optionally passing a result to the popup opener.


```typescript
Expand Down
126 changes: 97 additions & 29 deletions projects/scion/e2e-testing/src/workbench-client/host-popup.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,46 +39,114 @@ test.describe('Workbench Host Popup', () => {
await expectPopup(popupPage).toBeVisible();
});

test('should allow closing the popup and returning a value to the popup opener', async ({appPO, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});
test.describe('popup result', () => {
test('should allow closing the popup and returning a value to the popup opener', async ({appPO, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// TODO [#271]: Register popup capability in the host app via RegisterWorkbenchCapabilityPagePO when implemented the issue #271
// https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/271
// TODO [#271]: Register popup capability in the host app via RegisterWorkbenchCapabilityPagePO when implemented the issue #271
// https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/271

await microfrontendNavigator.registerIntention('app1', {type: 'popup', qualifier: {component: 'host-popup'}});
await microfrontendNavigator.registerIntention('app1', {type: 'popup', qualifier: {component: 'host-popup'}});

// open the popup
const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1');
await popupOpenerPage.enterQualifier({component: 'host-popup'});
await popupOpenerPage.enterCssClass('testee');
await popupOpenerPage.open();
// open the popup
const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1');
await popupOpenerPage.enterQualifier({component: 'host-popup'});
await popupOpenerPage.enterCssClass('testee');
await popupOpenerPage.open();

const popup = appPO.popup({cssClass: 'testee'});
const popupPage = new HostPopupPagePO(popup);
const popup = appPO.popup({cssClass: 'testee'});
const popupPage = new HostPopupPagePO(popup);

await popupPage.close({returnValue: 'RETURN VALUE'});
await expect(popupOpenerPage.returnValue).toHaveText('RETURN VALUE');
});
await popupPage.close({returnValue: 'RETURN VALUE'});
await expect(popupOpenerPage.returnValue).toHaveText('RETURN VALUE');
});

test('should allow closing the popup with an error', async ({appPO, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});
test('should allow closing the popup with an error', async ({appPO, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// TODO [#271]: Register popup capability in the host app via RegisterWorkbenchCapabilityPagePO when implemented the issue #271
// https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/271
// TODO [#271]: Register popup capability in the host app via RegisterWorkbenchCapabilityPagePO when implemented the issue #271
// https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/271

await microfrontendNavigator.registerIntention('app1', {type: 'popup', qualifier: {component: 'host-popup'}});
await microfrontendNavigator.registerIntention('app1', {type: 'popup', qualifier: {component: 'host-popup'}});

// open the popup
const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1');
await popupOpenerPage.enterQualifier({component: 'host-popup'});
await popupOpenerPage.enterCssClass('testee');
await popupOpenerPage.open();
// open the popup
const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1');
await popupOpenerPage.enterQualifier({component: 'host-popup'});
await popupOpenerPage.enterCssClass('testee');
await popupOpenerPage.open();

const popup = appPO.popup({cssClass: 'testee'});
const popupPage = new HostPopupPagePO(popup);
const popup = appPO.popup({cssClass: 'testee'});
const popupPage = new HostPopupPagePO(popup);

await popupPage.close({returnValue: 'ERROR', closeWithError: true});
await expect(popupOpenerPage.error).toHaveText('ERROR');
await popupPage.close({returnValue: 'ERROR', closeWithError: true});
await expect(popupOpenerPage.error).toHaveText('ERROR');
});

test('should allow returning value on focus loss', async ({appPO, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// TODO [#271]: Register popup capability in the host app via RegisterWorkbenchCapabilityPagePO when implemented the issue #271
// https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/271

await microfrontendNavigator.registerIntention('app1', {type: 'popup', qualifier: {component: 'host-popup'}});

// open the popup
const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1');
await popupOpenerPage.enterQualifier({component: 'host-popup'});
await popupOpenerPage.enterCssClass('testee');
await popupOpenerPage.open();

const popup = appPO.popup({cssClass: 'testee'});
const popupPage = new HostPopupPagePO(popup);
await popupPage.enterReturnValue('RETURN VALUE', {apply: true});

await popupOpenerPage.view.tab.click();
await expect(popupOpenerPage.returnValue).toHaveText('RETURN VALUE');
});

test('should return only the latest result value on close', async ({appPO, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// TODO [#271]: Register popup capability in the host app via RegisterWorkbenchCapabilityPagePO when implemented the issue #271
// https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/271

await microfrontendNavigator.registerIntention('app1', {type: 'popup', qualifier: {component: 'host-popup'}});

// open the popup
const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1');
await popupOpenerPage.enterQualifier({component: 'host-popup'});
await popupOpenerPage.enterCssClass('testee');
await popupOpenerPage.open();

const popup = appPO.popup({cssClass: 'testee'});
const popupPage = new HostPopupPagePO(popup);
await popupPage.enterReturnValue('RETURN VALUE 1', {apply: true});

await popupPage.close({returnValue: 'RETURN VALUE 2'});
await expect(popupOpenerPage.returnValue).toHaveText('RETURN VALUE 2');
});

test('should not return value on escape keystroke', async ({appPO, microfrontendNavigator, page}) => {
await appPO.navigateTo({microfrontendSupport: true});

// TODO [#271]: Register popup capability in the host app via RegisterWorkbenchCapabilityPagePO when implemented the issue #271
// https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/271

await microfrontendNavigator.registerIntention('app1', {type: 'popup', qualifier: {component: 'host-popup'}});

// open the popup
const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1');
await popupOpenerPage.enterQualifier({component: 'host-popup'});
await popupOpenerPage.enterCssClass('testee');
await popupOpenerPage.open();

const popup = appPO.popup({cssClass: 'testee'});
const popupPage = new HostPopupPagePO(popup);
await popupPage.enterReturnValue('RETURN VALUE', {apply: true});

await page.keyboard.press('Escape');
await expect(popupOpenerPage.returnValue).not.toBeAttached();
});
});

test('should stick to the popup anchor', async ({appPO, microfrontendNavigator}) => {
Expand Down
Loading

0 comments on commit ce5089e

Please sign in to comment.