From 0de58ed0b548c78264c19c0220e802dc1da177da Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Wed, 8 Dec 2021 17:07:22 +0100 Subject: [PATCH 1/6] feat(action-icon): add new web-component --- src/action-icon.ts | 182 ++++++++++++++++++ .../__snapshots__/action-icon.test.snap.js | 34 ++++ test/unit/action-icon.test.ts | 30 +++ 3 files changed, 246 insertions(+) create mode 100644 src/action-icon.ts create mode 100644 test/unit/__snapshots__/action-icon.test.snap.js create mode 100644 test/unit/action-icon.test.ts diff --git a/src/action-icon.ts b/src/action-icon.ts new file mode 100644 index 000000000..bd31f3e5b --- /dev/null +++ b/src/action-icon.ts @@ -0,0 +1,182 @@ +import { + css, + customElement, + html, + LitElement, + property, + TemplateResult, +} from 'lit-element'; +import { nothing } from 'lit-html'; + +import '@material/mwc-icon'; + +/** + * A responsive container rendering actions in a header. + * + * The "action" slot may contain up to eight icon buttons. + * The "icon" slot, if filled overrides the icon property. + * The default slot will be rendered into the pane body in a single column. + */ +@customElement('action-icon') +export class ActionIcon extends LitElement { + /** caption text, displayed in the header */ + @property({ type: String }) + label?: string; + /** icon name, displayed unless the "icon" slot is filled */ + @property({ type: String }) + icon?: string; + /** color header with secondary theme color while focus is within */ + @property({ type: Boolean }) + secondary = false; + /** highlight pane with dotted outline */ + @property({ type: Boolean }) + highlighted = false; + + async firstUpdated(): Promise { + this.tabIndex = 0; + } + + private renderIcon(): TemplateResult { + return html` + ${this.icon ? html`${this.icon}` : nothing} `; + } + + render(): TemplateResult { + return html`
+ ${this.renderIcon()} +
+
${this.label ?? nothing}
`; + } + + static styles = css` + :host { + display: flex; + flex-direction: column; + outline: none; + } + + section { + align-self: center; + } + + ::slotted([slot='icon']), + mwc-icon { + display: block; + color: var(--mdc-theme-on-surface); + transition: transform 150ms linear, box-shadow 200ms linear; + outline-color: var(--mdc-theme-primary); + outline-style: solid; + margin: 0px; + outline-width: 0px; + width: 64px; + height: 64px; + --mdc-icon-size: 64px; + } + + :host(:focus-within) ::slotted([slot='icon']), + :host(:focus-within) mwc-icon { + outline-style: solid; + outline-width: 4px; + transform: scale(0.8); + transition: all 250ms linear; + box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + } + + :host([secondary]) ::slotted([slot='icon']), + :host([secondary]) mwc-icon { + outline-color: var(--mdc-theme-secondary); + } + + :host([highlighted]) ::slotted([slot='icon']), + :host([highlighted]) mwc-icon { + outline-style: dotted; + outline-width: 2px; + } + + ::slotted([slot='icon']:hover), + mwc-icon:hover { + outline-style: dashed; + outline-width: 2px; + transition: transform 200ms linear, box-shadow 250ms linear; + } + + ::slotted([slot='action']) { + color: var(--mdc-theme-on-surface); + transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 200ms linear; + position: absolute; + pointer-events: none; + z-index: 1; + opacity: 0; + margin-top: -56px; + margin-left: 8px; + } + + :host(:focus-within) ::slotted([slot='action']) { + transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 250ms linear; + pointer-events: auto; + opacity: 1; + } + + :host(:focus-within) ::slotted([slot='action']:nth-of-type(1)) { + transform: translate(0px, -52px); + } + :host(:focus-within) ::slotted([slot='action']:nth-of-type(2)) { + transform: translate(0px, 52px); + } + :host(:focus-within) ::slotted([slot='action']:nth-of-type(3)) { + transform: translate(52px, 0px); + } + :host(:focus-within) ::slotted([slot='action']:nth-of-type(4)) { + transform: translate(-52px, 0px); + } + :host(:focus-within) ::slotted([slot='action']:nth-of-type(5)) { + transform: translate(52px, -52px); + } + :host(:focus-within) ::slotted([slot='action']:nth-of-type(6)) { + transform: translate(-52px, 52px); + } + :host(:focus-within) ::slotted([slot='action']:nth-of-type(7)) { + transform: translate(-52px, -52px); + } + :host(:focus-within) ::slotted([slot='action']:nth-of-type(8)) { + transform: translate(52px, 52px); + } + + footer { + color: var(--mdc-theme-on-surface); + font-family: 'Roboto', sans-serif; + font-weight: 300; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0px; + opacity: 1; + transition: opacity 200ms linear; + text-align: center; + align-self: center; + max-width: 64px; + transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 200ms linear; + direction: rtl; + } + + :host(:focus-within) footer { + max-width: 100vw; + transform: translate(0px, -140px); + background-color: var(--mdc-theme-secondary); + font-weight: 500; + color: var(--mdc-theme-on-secondary); + padding: 4px 8px; + border-radius: 4px; + font-size: 1.2em; + box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + } + `; +} diff --git a/test/unit/__snapshots__/action-icon.test.snap.js b/test/unit/__snapshots__/action-icon.test.snap.js new file mode 100644 index 000000000..2aa4b4307 --- /dev/null +++ b/test/unit/__snapshots__/action-icon.test.snap.js @@ -0,0 +1,34 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Basic component action-icon with icon property set looks like the latest snapshot"] = +`
+ + + + + + +
+
+
+`; +/* end snapshot Basic component action-icon with icon property set looks like the latest snapshot */ + +snapshots["Basic component action-icon with unset icon property looks like the latest snapshot"] = +`
+ + + + edit + + + + + +
+
+
+`; +/* end snapshot Basic component action-icon with unset icon property looks like the latest snapshot */ + diff --git a/test/unit/action-icon.test.ts b/test/unit/action-icon.test.ts new file mode 100644 index 000000000..046782112 --- /dev/null +++ b/test/unit/action-icon.test.ts @@ -0,0 +1,30 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import '../../src/action-icon.js'; +import { ActionIcon } from '../../src/action-icon.js'; + +describe('Basic component action-icon', () => { + let element: ActionIcon; + + beforeEach(async () => { + element = await fixture( + html`` + ); + await element.updateComplete; + }); + describe('with icon property set', () => { + it('looks like the latest snapshot', async () => { + await expect(element).shadowDom.to.equalSnapshot(); + }); + }); + + describe('with unset icon property ', () => { + beforeEach(async () => { + element.icon = 'edit'; + await element.requestUpdate(); + }); + it('looks like the latest snapshot', async () => { + await expect(element).shadowDom.to.equalSnapshot(); + }); + }); +}); From 11c9504a9e04617d9d54c4d144ab40e85bc35d35 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Thu, 9 Dec 2021 11:31:45 +0100 Subject: [PATCH 2/6] refactor(conducting-equipment-editor): use action-icon (#417) * refactor(conducting-equipment-editor): use action-icon * fix(condicting-equpment-editor): add missing import statement --- src/zeroline/conducting-equipment-editor.ts | 167 ++++-------------- .../ConductingEquipmentEditor.test.ts | 82 --------- .../conducting-equipment-editor.test.snap.js | 31 +--- .../conducting-equipment-editor.test.ts | 59 ++++++- 4 files changed, 95 insertions(+), 244 deletions(-) delete mode 100644 test/unit/zeroline/ConductingEquipmentEditor.test.ts diff --git a/src/zeroline/conducting-equipment-editor.ts b/src/zeroline/conducting-equipment-editor.ts index ef5184178..ef4880a32 100644 --- a/src/zeroline/conducting-equipment-editor.ts +++ b/src/zeroline/conducting-equipment-editor.ts @@ -9,32 +9,30 @@ import { import '@material/mwc-fab'; -import { BayEditor } from './bay-editor.js'; +import '../action-icon.js'; import { startMove, getIcon } from './foundation.js'; import { newActionEvent, newWizardEvent } from '../foundation.js'; +import { BayEditor } from './bay-editor.js'; import { wizards } from '../wizards/wizard-library.js'; /** [[`SubstationEditor`]] subeditor for a `ConductingEquipment` element. */ @customElement('conducting-equipment-editor') export class ConductingEquipmentEditor extends LitElement { - @property({ type: Element }) + /** SCL element ConductingEquipment */ + @property({ attribute: false }) element!: Element; - - @property({ type: Boolean }) - readonly = false; - + /** ConductingEquipment name attribute */ @property({ type: String }) get name(): string { return this.element.getAttribute('name') ?? ''; } - openEditWizard(): void { + private openEditWizard(): void { const wizard = wizards['ConductingEquipment'].edit(this.element); if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } - /** Opens a [[`WizardDialog`]] for editing `LNode` connections. */ - openLNodeWizard(): void { + private openLNodeWizard(): void { const wizard = wizards['LNode'].edit(this.element); if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } @@ -53,132 +51,37 @@ export class ConductingEquipmentEditor extends LitElement { } render(): TemplateResult { - return html` -
- ${getIcon(this.element)} - ${this.readonly - ? html`` - : html` - - - `} -
-

${this.name}

- `; + return html` + ${getIcon(this.element)} + + + + + `; } static styles = css` - #container { - color: var(--mdc-theme-on-surface); - width: 64px; - height: 64px; - margin: auto; - position: relative; - transition: all 200ms linear; - } - - #container:focus { - outline: none; - } - - #container > svg { - color: var(--mdc-theme-on-surface); - width: 64px; - height: 64px; - transition: transform 150ms linear, box-shadow 200ms linear; - outline-color: var(--mdc-theme-primary); - outline-style: solid; - outline-width: 0px; - } - - #container:focus > svg { - box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); - } - - #container:hover > svg { - outline: 2px dashed var(--mdc-theme-primary); - transition: transform 200ms linear, box-shadow 250ms linear; - } - - #container:focus-within > svg { - outline: 2px solid var(--mdc-theme-primary); - background: var(--mdc-theme-on-primary); - transform: scale(0.8); - transition: transform 200ms linear, box-shadow 250ms linear; - } - - .menu-item { - color: var(--mdc-theme-on-surface); - transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 200ms linear; - position: absolute; - top: 8px; - left: 8px; - pointer-events: none; - z-index: 1; - opacity: 0; - } - - #container:focus-within > .menu-item { - transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 250ms linear; - pointer-events: auto; - opacity: 1; - } - - #container:focus-within > .menu-item.up { - transform: translate(0px, -52px); - } - - #container:focus-within > .menu-item.down { - transform: translate(0px, 52px); - } - - #container:focus-within > .menu-item.right { - transform: translate(52px, 0px); - } - - #container:focus-within > .menu-item.left { - transform: translate(-52px, 0px); - } - - h4 { - color: var(--mdc-theme-on-surface); - font-family: 'Roboto', sans-serif; - font-weight: 300; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0px; - opacity: 1; - transition: opacity 200ms linear; - text-align: center; - } - - :host(.moving) #container, - :host(.moving) h4 { + :host(.moving) { opacity: 0.3; } `; diff --git a/test/unit/zeroline/ConductingEquipmentEditor.test.ts b/test/unit/zeroline/ConductingEquipmentEditor.test.ts deleted file mode 100644 index 943e6036d..000000000 --- a/test/unit/zeroline/ConductingEquipmentEditor.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { fixture, html, expect } from '@open-wc/testing'; - -import '../../../src/wizard-textfield.js'; -import { WizardInput, isCreate, isUpdate } from '../../../src/foundation.js'; -import { createAction } from '../../../src/wizards/conductingequipment.js'; -import { updateNamingAction } from '../../../src/wizards/foundation/actions.js'; - -describe('ConductingEquipmentEditor', () => { - const noOp = () => { - return; - }; - const newWizard = (done = noOp) => { - const element = document.createElement('mwc-dialog'); - element.close = done; - return element; - }; - - let inputs: WizardInput[]; - beforeEach(async () => { - inputs = await Promise.all( - ['name', 'desc', 'type'].map( - label => - >( - fixture(html``) - ) - ) - ); - inputs[2] = await fixture( - html`"Circuit Breaker"` - ); - }); - - describe('createAction', () => { - let parent: Element; - beforeEach(() => { - parent = new DOMParser().parseFromString( - '', - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction which returns a Create EditorAction', () => { - const wizardAction = createAction(parent); - expect(wizardAction(inputs, newWizard())[0]).to.satisfy(isCreate); - }); - }); - - describe('updateAction', () => { - let element: Element; - beforeEach(() => { - element = new DOMParser().parseFromString( - '', - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction which retruns one EditorActions', () => { - const wizardAction = updateNamingAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(1); - }); - - it('returns a WizardAction with returned EditorAction beeing an Update', () => { - const wizardAction = updateNamingAction(element); - expect(wizardAction(inputs, newWizard())[0]).to.satisfy(isUpdate); - }); - - describe('with no change in ConductingEquipement', () => { - let element: Element; - beforeEach(() => { - element = new DOMParser().parseFromString( - '', - 'application/xml' - ).documentElement; - }); - - it('returns a WizardAction with an empty EditorActions array', () => { - const wizardAction = updateNamingAction(element); - expect(wizardAction(inputs, newWizard()).length).to.equal(0); - }); - }); - }); -}); diff --git a/test/unit/zeroline/__snapshots__/conducting-equipment-editor.test.snap.js b/test/unit/zeroline/__snapshots__/conducting-equipment-editor.test.snap.js index af7884039..b6c01f6cb 100644 --- a/test/unit/zeroline/__snapshots__/conducting-equipment-editor.test.snap.js +++ b/test/unit/zeroline/__snapshots__/conducting-equipment-editor.test.snap.js @@ -2,50 +2,37 @@ export const snapshots = {}; snapshots["conducting-equipment-editor looks like the latest snapshot"] = -`
+ + -
-

- QA1 -

+ `; /* end snapshot conducting-equipment-editor looks like the latest snapshot */ -snapshots["conducting-equipment-editor with readonly property looks like the latest snapshot"] = -`
-
-

- QA1 -

-`; -/* end snapshot conducting-equipment-editor with readonly property looks like the latest snapshot */ - diff --git a/test/unit/zeroline/conducting-equipment-editor.test.ts b/test/unit/zeroline/conducting-equipment-editor.test.ts index 3067981bb..e6603b74a 100644 --- a/test/unit/zeroline/conducting-equipment-editor.test.ts +++ b/test/unit/zeroline/conducting-equipment-editor.test.ts @@ -1,16 +1,22 @@ import { fixture, html, expect } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; import '../../../src/zeroline/conducting-equipment-editor.js'; import { ConductingEquipmentEditor } from '../../../src/zeroline/conducting-equipment-editor.js'; +import { isDelete } from '../../../src/foundation.js'; describe('conducting-equipment-editor', () => { let element: ConductingEquipmentEditor; let validSCL: XMLDocument; + let wizardEvent: SinonSpy; + let actionEvent: SinonSpy; + beforeEach(async () => { validSCL = await fetch('/test/testfiles/valid2007B4.scd') .then(response => response.text()) .then(str => new DOMParser().parseFromString(str, 'application/xml')); + element = ( await fixture( html` { >` ) ); + + wizardEvent = spy(); + window.addEventListener('wizard', wizardEvent); + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); }); it('looks like the latest snapshot', async () => { await expect(element).shadowDom.to.equalSnapshot(); }); - describe('with readonly property', () => { - beforeEach(async () => { - element.readonly = true; - await element.requestUpdate(); - }); - it('looks like the latest snapshot', async () => { - await expect(element).shadowDom.to.equalSnapshot(); - }); + it('renders empty string in case ConductingEquipment name attribute is missing', async () => { + const condEq = validSCL.querySelector('ConductingEquipment'); + condEq?.removeAttribute('name'); + await element.requestUpdate(); + + expect(element).to.have.property('name', ''); + }); + + it('triggers edit wizard for LNode element on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[icon="account_tree"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.have.be.calledOnce; + expect(wizardEvent.args[0][0].detail.wizard[0].title).to.contain('lnode'); + }); + + it('triggers edit wizard for ConductingEquipment element on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[icon="edit"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.have.be.calledOnce; + expect(wizardEvent.args[0][0].detail.wizard[0].title).to.contain('edit'); + }); + + it('triggers remove action on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[icon="delete"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.not.have.been.called; + expect(actionEvent).to.have.been.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isDelete); }); }); From f0d09b6a1a965d6a413297685666ab9f8f848c73 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Thu, 9 Dec 2021 13:52:39 +0100 Subject: [PATCH 3/6] refactor(ied-editor): use action-icon (#418) --- src/zeroline/ied-editor.ts | 161 +++--------------- .../__snapshots__/ied-editor.test.snap.js | 28 +++ test/unit/zeroline/ied-editor.test.ts | 82 +++++++++ 3 files changed, 135 insertions(+), 136 deletions(-) create mode 100644 test/unit/zeroline/__snapshots__/ied-editor.test.snap.js create mode 100644 test/unit/zeroline/ied-editor.test.ts diff --git a/src/zeroline/ied-editor.ts b/src/zeroline/ied-editor.ts index 02ed1ef2b..bdff48db5 100644 --- a/src/zeroline/ied-editor.ts +++ b/src/zeroline/ied-editor.ts @@ -1,5 +1,4 @@ import { - css, customElement, html, LitElement, @@ -12,25 +11,27 @@ import '@material/mwc-fab'; import '@material/mwc-icon'; import { Fab } from '@material/mwc-fab'; +import '../action-icon.js'; import { createClientLnWizard } from '../wizards/clientln.js'; import { gooseIcon } from '../icons.js'; import { newWizardEvent } from '../foundation.js'; import { selectGseControlWizard } from '../wizards/gsecontrol.js'; -/** [[`SubstationEditor`]] subeditor for a `ConductingEquipment` element. */ +/** [[`SubstationEditor`]] subeditor for a child-less `IED` element. */ @customElement('ied-editor') export class IedEditor extends LitElement { - @property({ type: Element }) + /** SCL element IED */ + @property({ attribute: false }) element!: Element; - + /** IED name attribute */ @property({ type: String }) get name(): string { - return this.element.getAttribute('name') ?? ''; + return this.element.getAttribute('name') ?? 'UNDEFINED'; } - @query('#connectreport') connectReport!: Fab; + @query('.connectreport') connectReport!: Fab; - openCommunicationMapping(): void { + private openCommunicationMapping(): void { const sendingIeds = Array.from( this.element.closest('SCL')?.querySelectorAll('IED') ?? [] ); @@ -38,139 +39,27 @@ export class IedEditor extends LitElement { if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } - openGseControlSelection(): void { + private openGseControlSelection(): void { const wizard = selectGseControlWizard(this.element); if (wizard) this.dispatchEvent(newWizardEvent(wizard)); } render(): TemplateResult { - return html` -
- - developer_board - - ${gooseIcon} -
-

${this.name}

- `; + return html` + ${gooseIcon} `; } - - static styles = css` - #container { - color: var(--mdc-theme-on-surface); - width: 50px; - height: 50px; - margin: auto; - position: relative; - transition: all 200ms linear; - user-select: none; - } - - #container:focus { - outline: none; - } - - .icon { - color: var(--mdc-theme-on-surface); - --mdc-icon-size: 50px; - transition: transform 150ms linear, box-shadow 200ms linear; - outline-color: var(--mdc-theme-primary); - outline-style: solid; - outline-width: 0px; - } - - #container > .icon { - color: var(--mdc-theme-on-surface); - width: 50px; - height: 50px; - transition: transform 150ms linear, box-shadow 200ms linear; - outline-color: var(--mdc-theme-primary); - outline-style: solid; - outline-width: 0px; - } - - #container:focus > .icon { - box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); - } - - #container:hover > .icon { - outline: 2px dashed var(--mdc-theme-primary); - transition: transform 200ms linear, box-shadow 250ms linear; - } - - #container:focus-within > .icon { - outline: 2px solid var(--mdc-theme-primary); - background: var(--mdc-theme-on-primary); - transform: scale(0.8); - transition: transform 200ms linear, box-shadow 250ms linear; - } - - .menu-item { - color: var(--mdc-theme-on-surface); - transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 200ms linear; - position: absolute; - top: 2px; - left: 2px; - pointer-events: none; - z-index: 1; - opacity: 0; - } - - #container:focus-within > .menu-item { - transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 250ms linear; - pointer-events: auto; - opacity: 1; - } - - #container:focus-within > .menu-item.up { - transform: translate(0px, -60px); - } - - #container:focus-within > .menu-item.down { - transform: translate(0px, 60px); - } - - #container:focus-within > .menu-item.right { - transform: translate(60px, 0px); - } - - #container:focus-within > .menu-item.left { - transform: translate(-60px, 0px); - } - - h4 { - color: var(--mdc-theme-on-surface); - font-family: 'Roboto', sans-serif; - font-weight: 300; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0px; - opacity: 1; - transition: opacity 200ms linear; - text-align: center; - direction: rtl; - } - - :host(.moving) #container, - :host(.moving) h4 { - opacity: 0.3; - } - `; } diff --git a/test/unit/zeroline/__snapshots__/ied-editor.test.snap.js b/test/unit/zeroline/__snapshots__/ied-editor.test.snap.js new file mode 100644 index 000000000..16b24bc05 --- /dev/null +++ b/test/unit/zeroline/__snapshots__/ied-editor.test.snap.js @@ -0,0 +1,28 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["A component to visualize SCL element IED looks like the latest snapshot"] = +` + + + + + + + +`; +/* end snapshot A component to visualize SCL element IED looks like the latest snapshot */ + diff --git a/test/unit/zeroline/ied-editor.test.ts b/test/unit/zeroline/ied-editor.test.ts new file mode 100644 index 000000000..34aca2e04 --- /dev/null +++ b/test/unit/zeroline/ied-editor.test.ts @@ -0,0 +1,82 @@ +import { fixture, html, expect } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; + +import '../../../src/zeroline/ied-editor.js'; +import { IedEditor } from '../../../src/zeroline/ied-editor.js'; + +describe('A component to visualize SCL element IED', () => { + let element: IedEditor; + let validSCL: XMLDocument; + + let wizardEvent: SinonSpy; + + beforeEach(async () => { + validSCL = await fetch('/test/testfiles/valid2007B4.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + element = ( + await fixture( + html`` + ) + ); + + wizardEvent = spy(); + window.addEventListener('wizard', wizardEvent); + }); + + it('looks like the latest snapshot', async () => { + await expect(element).shadowDom.to.equalSnapshot(); + }); + + it('renders label UNDEFINED in case IED name attribute is missing', async () => { + const condEq = validSCL.querySelector('IED'); + condEq?.removeAttribute('name'); + await element.requestUpdate(); + + expect(element).to.have.property('name', 'UNDEFINED'); + }); + + it('triggers select wizard for GSEControl element on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[class="selectgse"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.have.be.calledOnce; + expect(wizardEvent.args[0][0].detail.wizard[0].title).to.contain('select'); + }); + + it('triggers create wizard for ClientLN element on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[class="connectreport"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.have.be.calledOnce; + expect(wizardEvent.args[0][0].detail.wizard[0].title).to.contain( + 'connectToIED' + ); + }); + + it('still triggers create wizard for ClientLN element with missing parent', async () => { + const copyElement: Element = element.cloneNode(true); + element.element = copyElement; + await element.requestUpdate(); + + (( + element.shadowRoot?.querySelector('mwc-fab[class="connectreport"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.have.been.calledOnce; + expect(wizardEvent.args[0][0].detail.wizard[0].title).to.contain( + 'connectToIED' + ); + }); +}); From cb37c6aa9cbce45aae635637805aa46395b5bf66 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Mon, 13 Dec 2021 16:15:57 +0100 Subject: [PATCH 4/6] refactor(communication/connectedap-editor): use action-icon (#420) --- .../communication/connectedap-editor.ts | 123 ++---------------- .../conductingap-editor.test.snap.js | 21 ++- .../communication/conductingap-editor.test.ts | 51 ++++++-- 3 files changed, 64 insertions(+), 131 deletions(-) diff --git a/src/editors/communication/connectedap-editor.ts b/src/editors/communication/connectedap-editor.ts index 547cfc8c7..454f1b088 100644 --- a/src/editors/communication/connectedap-editor.ts +++ b/src/editors/communication/connectedap-editor.ts @@ -4,7 +4,6 @@ import { customElement, html, property, - css, } from 'lit-element'; import { ifDefined } from 'lit-html/directives/if-defined'; import { translate, get } from 'lit-translate'; @@ -19,6 +18,7 @@ import { Checkbox } from '@material/mwc-checkbox'; import { List } from '@material/mwc-list'; import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; +import '../../action-icon.js'; import '../../wizard-textfield.js'; import '../../filtered-list.js'; import { @@ -276,15 +276,16 @@ function editConnectedApWizard(element: Element): Wizard { /** [[`Communication`]] subeditor for a `ConnectedAP` element. */ @customElement('connectedap-editor') export class ConnectedAPEditor extends LitElement { - @property() + /** SCL element ConnectedAP */ + @property({ attribute: false }) element!: Element; - - @property() - get apName(): string | null { - return this.element.getAttribute('apName') ?? null; + /** ConductingEquipment apName attribute */ + @property({ type: String }) + get apName(): string { + return this.element.getAttribute('apName') ?? 'UNDEFINED'; } - openEditWizard(): void { + private openEditWizard(): void { this.dispatchEvent(newWizardEvent(editConnectedApWizard(this.element))); } @@ -303,116 +304,20 @@ export class ConnectedAPEditor extends LitElement { render(): TemplateResult { return html` -
- settings_input_hdmi - -
-

${this.apName}

+ > `; } - - static styles = css` - #container { - color: var(--mdc-theme-on-surface); - width: 64px; - height: 64px; - margin: auto; - position: relative; - transition: all 200ms linear; - } - - #container:focus { - outline: none; - } - - .fancy { - color: var(--mdc-theme-on-surface); - --mdc-icon-size: 64px; - transition: transform 150ms linear, box-shadow 200ms linear; - outline-color: var(--mdc-theme-primary); - outline-style: solid; - outline-width: 0px; - } - - #container:focus > .fancy { - box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); - } - - #container:hover > .fancy { - outline: 2px dashed var(--mdc-theme-primary); - transition: transform 200ms linear, box-shadow 250ms linear; - } - - #container:focus-within > .fancy { - outline: 2px solid var(--mdc-theme-primary); - background: var(--mdc-theme-on-primary); - transform: scale(0.8); - transition: transform 200ms linear, box-shadow 250ms linear; - } - - .menu-item { - color: var(--mdc-theme-on-surface); - transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 200ms linear; - position: absolute; - top: 8px; - left: 8px; - pointer-events: none; - z-index: 1; - opacity: 0; - } - - #container:focus-within > .menu-item { - transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 250ms linear; - pointer-events: auto; - opacity: 1; - } - - #container:focus-within > .menu-item.up { - transform: translate(0px, -52px); - } - - #container:focus-within > .menu-item.down { - transform: translate(0px, 52px); - } - - #container:focus-within > .menu-item.right { - transform: translate(52px, 0px); - } - - #container:focus-within > .menu-item.left { - transform: translate(-52px, 0px); - } - - h4 { - color: var(--mdc-theme-on-surface); - font-family: 'Roboto', sans-serif; - font-weight: 300; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0px; - opacity: 1; - transition: opacity 200ms linear; - text-align: center; - } - - :host(.moving) #container, - :host(.moving) h4 { - opacity: 0.3; - } - `; } diff --git a/test/unit/editors/communication/__snapshots__/conductingap-editor.test.snap.js b/test/unit/editors/communication/__snapshots__/conductingap-editor.test.snap.js index 94ee7a36d..532ea49c4 100644 --- a/test/unit/editors/communication/__snapshots__/conductingap-editor.test.snap.js +++ b/test/unit/editors/communication/__snapshots__/conductingap-editor.test.snap.js @@ -1,30 +1,25 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["connectedap-editor looks like the latest snapshot"] = -`
- - settings_input_hdmi - -
-

- P1 -

+ `; -/* end snapshot connectedap-editor looks like the latest snapshot */ +/* end snapshot A component to visualize SCL element ConnectedAP looks like the latest snapshot */ diff --git a/test/unit/editors/communication/conductingap-editor.test.ts b/test/unit/editors/communication/conductingap-editor.test.ts index c9fb98624..1099c301b 100644 --- a/test/unit/editors/communication/conductingap-editor.test.ts +++ b/test/unit/editors/communication/conductingap-editor.test.ts @@ -1,12 +1,17 @@ import { fixture, html, expect } from '@open-wc/testing'; +import { SinonSpy, spy } from 'sinon'; import '../../../../src/editors/communication/connectedap-editor.js'; import { ConnectedAPEditor } from '../../../../src/editors/communication/connectedap-editor.js'; +import { isDelete } from '../../../../src/foundation.js'; -describe('connectedap-editor', () => { +describe('A component to visualize SCL element ConnectedAP', () => { let element: ConnectedAPEditor; let validSCL: XMLDocument; + let wizardEvent: SinonSpy; + let actionEvent: SinonSpy; + beforeEach(async () => { validSCL = await fetch('/test/testfiles/valid2007B4.scd') .then(response => response.text()) @@ -20,17 +25,45 @@ describe('connectedap-editor', () => { >` ) ); - }); - it('has a apName property', () => - expect(element).to.have.property( - 'apName', - validSCL - .querySelector('SubNetwork[name="StationBus"] > ConnectedAP') - ?.getAttribute('apName') - )); + wizardEvent = spy(); + window.addEventListener('wizard', wizardEvent); + actionEvent = spy(); + window.addEventListener('editor-action', actionEvent); + }); it('looks like the latest snapshot', async () => { await expect(element).shadowDom.to.equalSnapshot(); }); + + it('renders label UNDEFINED in case ConnectedAP apName attribute is missing', async () => { + const connAp = validSCL.querySelector('ConnectedAP'); + connAp?.removeAttribute('apName'); + await element.requestUpdate(); + + expect(element).to.have.property('apName', 'UNDEFINED'); + }); + + it('triggers edit wizard for ConnectedAP element on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[icon="edit"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.have.be.calledOnce; + expect(wizardEvent.args[0][0].detail.wizard[0].title).to.contain('edit'); + }); + + it('triggers remove action on action button click', async () => { + (( + element.shadowRoot?.querySelector('mwc-fab[icon="delete"]') + )).click(); + + await element.requestUpdate(); + + expect(wizardEvent).to.not.have.been.called; + expect(actionEvent).to.have.been.calledOnce; + expect(actionEvent.args[0][0].detail.action).to.satisfy(isDelete); + }); }); From 508a2bbaea0f2c181dd1ecfb8b93d9ddc89d5411 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Wed, 15 Dec 2021 16:03:57 +0100 Subject: [PATCH 5/6] style(action-icon): make sure footer transform does not effect parent CSS (#419) * fix(action-icon): make sure footer transform does not effect other CSS * refactor(action-icon): improve styling * refactor(action-icon): adjust action timing for header * test(action-icon): adapt snapshots --- src/action-icon.ts | 26 +++++++++++++------ .../__snapshots__/action-icon.test.snap.js | 8 ++++-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/action-icon.ts b/src/action-icon.ts index bd31f3e5b..dd1688a07 100644 --- a/src/action-icon.ts +++ b/src/action-icon.ts @@ -45,9 +45,8 @@ export class ActionIcon extends LitElement { } render(): TemplateResult { - return html`
- ${this.renderIcon()} -
+ return html`
${this.label ?? nothing}
+
${this.renderIcon()}
${this.label ?? nothing}
`; } @@ -157,26 +156,37 @@ export class ActionIcon extends LitElement { text-overflow: ellipsis; margin: 0px; opacity: 1; - transition: opacity 200ms linear; text-align: center; align-self: center; max-width: 64px; - transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 200ms linear; direction: rtl; } - :host(:focus-within) footer { + header { + position: absolute; + text-align: center; + align-self: center; max-width: 100vw; - transform: translate(0px, -140px); background-color: var(--mdc-theme-secondary); font-weight: 500; color: var(--mdc-theme-on-secondary); padding: 4px 8px; border-radius: 4px; font-size: 1.2em; + opacity: 0; + transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 200ms linear; + } + + :host(:focus-within) header { + display: initial; + position: absolute; + opacity: 1; + transform: translate(0, -80px); box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 250ms linear; } `; } diff --git a/test/unit/__snapshots__/action-icon.test.snap.js b/test/unit/__snapshots__/action-icon.test.snap.js index 2aa4b4307..3ece266a8 100644 --- a/test/unit/__snapshots__/action-icon.test.snap.js +++ b/test/unit/__snapshots__/action-icon.test.snap.js @@ -2,7 +2,9 @@ export const snapshots = {}; snapshots["Basic component action-icon with icon property set looks like the latest snapshot"] = -`
+`
+
+
@@ -16,7 +18,9 @@ snapshots["Basic component action-icon with icon property set looks like the lat /* end snapshot Basic component action-icon with icon property set looks like the latest snapshot */ snapshots["Basic component action-icon with unset icon property looks like the latest snapshot"] = -`
+`
+
+
From 818352630c4eeb81bb8c6460f303ad38caf97394 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Wed, 15 Dec 2021 16:24:25 +0100 Subject: [PATCH 6/6] style(action-icon): minor changes --- src/action-icon.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/action-icon.ts b/src/action-icon.ts index dd1688a07..ee8acbedb 100644 --- a/src/action-icon.ts +++ b/src/action-icon.ts @@ -155,7 +155,6 @@ export class ActionIcon extends LitElement { white-space: nowrap; text-overflow: ellipsis; margin: 0px; - opacity: 1; text-align: center; align-self: center; max-width: 64px; @@ -163,23 +162,33 @@ export class ActionIcon extends LitElement { } header { + color: var(--mdc-theme-on-primary); + background-color: var(--mdc-theme-primary); + font-family: 'Roboto', sans-serif; + font-weight: 500; + font-size: 1.2em; position: absolute; text-align: center; align-self: center; max-width: 100vw; - background-color: var(--mdc-theme-secondary); - font-weight: 500; - color: var(--mdc-theme-on-secondary); padding: 4px 8px; border-radius: 4px; - font-size: 1.2em; opacity: 0; transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms linear; } + :host(:hover) header { + position: absolute; + opacity: 1; + transform: translate(0, -40px); + box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 250ms linear; + } + :host(:focus-within) header { - display: initial; position: absolute; opacity: 1; transform: translate(0, -80px);