diff --git a/packages/angular-test-app/src/app/app-routing.module.ts b/packages/angular-test-app/src/app/app-routing.module.ts index c7a39bee3d9..b2f7d6e57a9 100644 --- a/packages/angular-test-app/src/app/app-routing.module.ts +++ b/packages/angular-test-app/src/app/app-routing.module.ts @@ -106,6 +106,7 @@ import VerticalTabsWithAvatar from 'src/preview-examples/vertical-tabs-with-avat import Workflow from 'src/preview-examples/workflow'; import WorkflowVertical from 'src/preview-examples/workflow-vertical'; import { NavigationTestComponent } from './components/navigation-test.component'; +import InformationBar from "../preview-examples/information-bar"; const routes: Routes = [ { @@ -243,6 +244,7 @@ const routes: Routes = [ { path: 'event-list-selected', component: EventListSelected }, { path: 'event-list', component: EventList }, { path: 'expanding-search', component: ExpandingSearch }, + { path: 'information-bar', component: InformationBar }, { path: 'flip-tile', component: FlipTile }, { path: 'group-context-menu', component: GroupContextMenu }, { path: 'group-custom-entry', component: GroupCustomEntry }, diff --git a/packages/angular-test-app/src/app/app.module.ts b/packages/angular-test-app/src/app/app.module.ts index 4c7208c71d9..cacc7fc45d1 100644 --- a/packages/angular-test-app/src/app/app.module.ts +++ b/packages/angular-test-app/src/app/app.module.ts @@ -114,6 +114,7 @@ import VerticalTabsWithAvatar from 'src/preview-examples/vertical-tabs-with-avat import Workflow from 'src/preview-examples/workflow'; import WorkflowVertical from 'src/preview-examples/workflow-vertical'; import { NavigationTestComponent } from './components/navigation-test.component'; +import InformationBar from "../preview-examples/information-bar"; @NgModule({ declarations: [ @@ -153,6 +154,7 @@ import { NavigationTestComponent } from './components/navigation-test.component' EventListSelected, EventList, ExpandingSearch, + InformationBar, FlipTile, GroupContextMenu, GroupCustomEntry, diff --git a/packages/angular-test-app/src/preview-examples/information-bar.ts b/packages/angular-test-app/src/preview-examples/information-bar.ts new file mode 100644 index 00000000000..af72259d32e --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/information-bar.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2023 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-example', + template: ` + + `, +}) +export default class InformationBar {} diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 2444e44c33f..2888821243f 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -1063,6 +1063,28 @@ export class IxIconButton { export declare interface IxIconButton extends Components.IxIconButton {} +@ProxyCmp({ + inputs: ['bar'] +}) +@Component({ + selector: 'ix-information-bar', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['bar'], +}) +export class IxInformationBar { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IxInformationBar extends Components.IxInformationBar {} + + @ProxyCmp({ }) @Component({ diff --git a/packages/angular/src/declare-components.ts b/packages/angular/src/declare-components.ts index 4657d7ae2ee..354b8fc7c67 100644 --- a/packages/angular/src/declare-components.ts +++ b/packages/angular/src/declare-components.ts @@ -42,6 +42,7 @@ export const DIRECTIVES = [ d.IxGroupItem, d.IxIcon, d.IxIconButton, + d.IxInformationBar, d.IxInputGroup, d.IxKeyValue, d.IxKeyValueList, diff --git a/packages/core/component-doc.json b/packages/core/component-doc.json index 8286df85369..a85ee7e845b 100644 --- a/packages/core/component-doc.json +++ b/packages/core/component-doc.json @@ -5344,6 +5344,52 @@ "parts": [], "listeners": [] }, + { + "dirPath": "./src/components/information-bar", + "filePath": "./src/components/information-bar/information-bar.tsx", + "fileName": "information-bar.tsx", + "readmePath": "./src/components/information-bar/readme.md", + "usagesDir": "./src/components/information-bar/usage", + "tag": "ix-information-bar", + "overview": "", + "usage": {}, + "docs": "", + "docsTags": [], + "encapsulation": "shadow", + "dependents": [], + "dependencies": [ + "ix-icon" + ], + "dependencyGraph": { + "ix-information-bar": [ + "ix-icon" + ] + }, + "props": [ + { + "name": "bar", + "type": "BarNumbers[]", + "mutable": false, + "reflectToAttr": false, + "docs": "Configuration of the bar", + "docsTags": [], + "default": "[]", + "values": [ + { + "type": "BarNumbers[]" + } + ], + "optional": false, + "required": false + } + ], + "methods": [], + "events": [], + "styles": [], + "slots": [], + "parts": [], + "listeners": [] + }, { "dirPath": "./src/components/input-group", "filePath": "./src/components/input-group/input-group.tsx", diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 5f41b9e1a8f..cb693679888 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -25,6 +25,7 @@ import { EmptyStateLayout } from "./components/empty-state/empty-state"; import { FlipTileState } from "./components/flip-tile/flip-tile-state"; import { IconButtonVariant } from "./components/icon-button/icon-button"; import { IndexButtonVariant } from "./components/index-button/index-button"; +import { BarNumbers } from "./components/information-bar/information-bar"; import { KeyValueLabelPosition } from "./components/key-value/key-value"; import { NotificationColor } from "./components/utils/notification-color"; import { ModalConfig, ModalInstance } from "./components/modal/modal-utils"; @@ -56,6 +57,7 @@ export { EmptyStateLayout } from "./components/empty-state/empty-state"; export { FlipTileState } from "./components/flip-tile/flip-tile-state"; export { IconButtonVariant } from "./components/icon-button/icon-button"; export { IndexButtonVariant } from "./components/index-button/index-button"; +export { BarNumbers } from "./components/information-bar/information-bar"; export { KeyValueLabelPosition } from "./components/key-value/key-value"; export { NotificationColor } from "./components/utils/notification-color"; export { ModalConfig, ModalInstance } from "./components/modal/modal-utils"; @@ -994,6 +996,12 @@ export namespace Components { */ "variant": IndexButtonVariant; } + interface IxInformationBar { + /** + * Configuration of the bar + */ + "bar": BarNumbers[]; + } interface IxInputGroup { } /** @@ -2455,6 +2463,12 @@ declare global { prototype: HTMLIxIndexButtonElement; new (): HTMLIxIndexButtonElement; }; + interface HTMLIxInformationBarElement extends Components.IxInformationBar, HTMLStencilElement { + } + var HTMLIxInformationBarElement: { + prototype: HTMLIxInformationBarElement; + new (): HTMLIxInformationBarElement; + }; interface HTMLIxInputGroupElement extends Components.IxInputGroup, HTMLStencilElement { } var HTMLIxInputGroupElement: { @@ -2791,6 +2805,7 @@ declare global { "ix-group-item": HTMLIxGroupItemElement; "ix-icon-button": HTMLIxIconButtonElement; "ix-index-button": HTMLIxIndexButtonElement; + "ix-information-bar": HTMLIxInformationBarElement; "ix-input-group": HTMLIxInputGroupElement; "ix-key-value": HTMLIxKeyValueElement; "ix-key-value-list": HTMLIxKeyValueListElement; @@ -3890,6 +3905,12 @@ declare namespace LocalJSX { */ "variant"?: IndexButtonVariant; } + interface IxInformationBar { + /** + * Configuration of the bar + */ + "bar"?: BarNumbers[]; + } interface IxInputGroup { } /** @@ -5002,6 +5023,7 @@ declare namespace LocalJSX { "ix-group-item": IxGroupItem; "ix-icon-button": IxIconButton; "ix-index-button": IxIndexButton; + "ix-information-bar": IxInformationBar; "ix-input-group": IxInputGroup; "ix-key-value": IxKeyValue; "ix-key-value-list": IxKeyValueList; @@ -5130,6 +5152,7 @@ declare module "@stencil/core" { "ix-group-item": LocalJSX.IxGroupItem & JSXBase.HTMLAttributes; "ix-icon-button": LocalJSX.IxIconButton & JSXBase.HTMLAttributes; "ix-index-button": LocalJSX.IxIndexButton & JSXBase.HTMLAttributes; + "ix-information-bar": LocalJSX.IxInformationBar & JSXBase.HTMLAttributes; "ix-input-group": LocalJSX.IxInputGroup & JSXBase.HTMLAttributes; /** * @since 1.6.0 diff --git a/packages/core/src/components/information-bar/information-bar.scss b/packages/core/src/components/information-bar/information-bar.scss new file mode 100644 index 00000000000..a846be8fb4d --- /dev/null +++ b/packages/core/src/components/information-bar/information-bar.scss @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2023 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +:host { + display: block; + overflow: hidden; + min-width: 200px; +} + +.bar-container { + display: flex; + flex-direction: row; + gap: 1px; +} + +.icon-container { + overflow: hidden; + display: flex; + margin-top: 4px; +} + +.icon-and-text { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.distance { + margin-right: 4px; +} + +.unassigned { + background-color: var(--theme-color-neutral); +} + +.unassigned-icon { + color: var(--theme-color-neutral); + margin-right: 4px; +} + +.unassigned-bar { + height: 8px; + background: repeating-linear-gradient( + -60deg, + var(--theme-color-neutral), + var(--theme-color-neutral) 2px, + var(--theme-color-inv-weak-text) 2px, + var(--theme-color-inv-weak-text) 4px + ); +} + +.alarm { + background-color: var(--theme-color-alarm); +} + +.alarm-icon { + color: var(--theme-color-alarm); + margin-right: 4px; +} + +.alarm-bar { + height: 8px; + background: repeating-linear-gradient( + -60deg, + var(--theme-color-alarm), + var(--theme-color-alarm) 2px, + var(--theme-color-inv-weak-text) 2px, + var(--theme-color-inv-weak-text) 4px + ); +} + +.critical { + background-color: var(--theme-color-critical); +} + +.critical-icon { + color: var(--theme-color-critical); + margin-right: 4px; +} + +.critical-bar { + height: 8px; + background: repeating-linear-gradient( + -60deg, + var(--theme-color-critical), + var(--theme-color-critical) 2px, + var(--theme-color-inv-weak-text) 2px, + var(--theme-color-inv-weak-text) 4px + ); +} + +.warning { + background-color: var(--theme-color-warning); +} + +.warning-icon { + color: var(--theme-color-warning); + margin-right: 4px; +} + +.warning-bar { + height: 8px; + background: repeating-linear-gradient( + -60deg, + var(--theme-color-warning), + var(--theme-color-warning) 2px, + var(--theme-color-inv-weak-text) 2px, + var(--theme-color-inv-weak-text) 4px + ); +} + +.info { + background-color: var(--theme-color-info); +} + +.info-icon { + color: var(--theme-color-info); + margin-right: 4px; +} + +.info-bar { + height: 8px; + background: repeating-linear-gradient( + -60deg, + var(--theme-color-info), + var(--theme-color-info) 2px, + var(--theme-color-inv-weak-text) 2px, + var(--theme-color-inv-weak-text) 4px + ); +} diff --git a/packages/core/src/components/information-bar/information-bar.tsx b/packages/core/src/components/information-bar/information-bar.tsx new file mode 100644 index 00000000000..f55a524a9a3 --- /dev/null +++ b/packages/core/src/components/information-bar/information-bar.tsx @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2023 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component, Element, h, Host, Prop, Watch } from '@stencil/core'; + +export type BarNumbers = { + count: number; + stripped: number; + icon: string; + color: string; +}; + +@Component({ + tag: 'ix-information-bar', + styleUrl: 'information-bar.scss', + shadow: true, +}) +export class InformationBar { + @Element() el: HTMLIxInformationBarElement; + + /** + * Configuration of the bar + */ + @Prop() bar: BarNumbers[] = []; + + @Watch('bar') + onBarChange() { + setTimeout(() => this.setPositionsOfIcons()); + } + + componentDidRender() { + setTimeout(() => this.setPositionsOfIcons()); + } + + setPositionsOfIcons() { + let currentBarWidth = 0; + let currentIconWidth = 0; + let sumOfMargins = 0; + const margins: number[] = []; + const iconElements = []; + + this.bar?.forEach((_, index) => { + const barElement = this.el.shadowRoot.querySelector('#bar-' + index); + + const iconElement = this.el.shadowRoot.querySelector('#icon-' + index); + + const barTileWidth = barElement?.getBoundingClientRect().width; + const iconTileWidth = iconElement?.getBoundingClientRect().width; + + const newBarWidth = currentBarWidth + barTileWidth; + const newIconWidth = currentIconWidth + iconTileWidth + sumOfMargins; + + const marginForIcon = + newBarWidth > newIconWidth ? newBarWidth - newIconWidth : 0; + iconElement?.setAttribute('style', `margin-right: ${marginForIcon}px`); + + currentBarWidth += barTileWidth; + currentIconWidth += iconTileWidth; + sumOfMargins += marginForIcon; + margins.push(marginForIcon); + iconElements.push(iconElement); + }); + + const overlap = currentIconWidth + sumOfMargins - currentBarWidth; + + if (overlap > 0) { + for (let i = margins.length - 1; i >= 0; i--) { + if (margins[i] > overlap) { + iconElements[i].setAttribute( + 'style', + `margin-right: ${margins[i] - overlap + 4}px` + ); + break; + } + } + } + + const iconContainer = this.el.shadowRoot.querySelector('#icon-container'); + iconContainer?.setAttribute('style', `visibility: visible`); + } + + sum() { + return this.bar.map((el) => el.count).reduce((acc, el) => acc + el); + } + + render() { + return ( + + + {this.bar?.map((bar, index) => { + return ( + + + + ); + })} + + + {this.bar?.map((bar, index) => { + return ( + + + {bar.count} + + ); + })} + + + ); + } +} diff --git a/packages/core/src/components/information-bar/test/information-bar.ct.ts b/packages/core/src/components/information-bar/test/information-bar.ct.ts new file mode 100644 index 00000000000..62fabadf94c --- /dev/null +++ b/packages/core/src/components/information-bar/test/information-bar.ct.ts @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2023 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { expect } from '@playwright/test'; +import { test } from '@utils/test'; + +const barNumbers = [ + { count: 50, stripped: 2, icon: 'alarm', color: 'alarm' }, + { count: 50, stripped: 2, icon: 'warning-rhomb', color: 'critical' }, + { count: 5, stripped: 2, icon: 'warning', color: 'warning' }, + { count: 5, stripped: 2, icon: 'info', color: 'info' }, + { count: 5, stripped: 2, icon: 'question', color: 'unassigned' }, +]; + +test('should render', async ({ mount, page }) => { + await mount(``); + const element = page.locator('ix-information-bar'); + await element.evaluate((element: HTMLIxInformationBarElement) => { + element.bar = [ + { count: 50, stripped: 2, icon: 'alarm', color: 'alarm' }, + { count: 50, stripped: 2, icon: 'warning-rhomb', color: 'critical' }, + { count: 5, stripped: 2, icon: 'warning', color: 'warning' }, + { count: 5, stripped: 2, icon: 'info', color: 'info' }, + { count: 5, stripped: 2, icon: 'question', color: 'unassigned' }, + ]; + }); + await expect(element).toHaveClass('hydrated'); +}); + +test('should have right width', async ({ mount, page }) => { + await mount(``); + const element = page.locator('ix-information-bar'); + await element.evaluate((element: HTMLIxInformationBarElement) => { + element.bar = [ + { count: 50, stripped: 2, icon: 'alarm', color: 'alarm' }, + { count: 50, stripped: 2, icon: 'warning-rhomb', color: 'critical' }, + { count: 5, stripped: 2, icon: 'warning', color: 'warning' }, + { count: 5, stripped: 2, icon: 'info', color: 'info' }, + { count: 5, stripped: 2, icon: 'question', color: 'unassigned' }, + ]; + }); + + for (const [index, _] of barNumbers.entries()) { + const bar = page.locator('#bar-' + index); + expect( + await bar.evaluate((node: HTMLElement) => node.getAttribute('class')) + ).toBe(barNumbers[index].color); + expect( + await bar.evaluate((node: HTMLElement) => node.getAttribute('style')) + ).toContain( + 'width: ' + Math.floor((100 * barNumbers[index].count) / sum()) + ); + } +}); + +test('should have right icons', async ({ mount, page }) => { + await mount(``); + const element = page.locator('ix-information-bar'); + await element.evaluate((element: HTMLIxInformationBarElement) => { + element.bar = [ + { count: 50, stripped: 2, icon: 'alarm', color: 'alarm' }, + { count: 50, stripped: 2, icon: 'warning-rhomb', color: 'critical' }, + { count: 5, stripped: 2, icon: 'warning', color: 'warning' }, + { count: 5, stripped: 2, icon: 'info', color: 'info' }, + { count: 5, stripped: 2, icon: 'question', color: 'unassigned' }, + ]; + }); + + for (const [index, _] of barNumbers.entries()) { + const icon = page.locator('#icon-' + index + ' i'); + expect( + await icon.evaluate((node: HTMLElement) => node.getAttribute('class')) + ).toContain(barNumbers[index].icon); + + const number = page.locator('#icon-' + index); + expect( + await number.evaluate((node: HTMLElement) => + node.children.item(0).getAttribute('class') + ) + ).toContain(barNumbers[index].color); + expect( + await number.evaluate((node: HTMLElement) => + node.children.item(0).getAttribute('name') + ) + ).toBe(barNumbers[index].icon); + } +}); + +function sum() { + return barNumbers.map((el) => el.count).reduce((acc, curr) => acc + curr); +} diff --git a/packages/core/src/components/information-bar/test/information-bar.spec.tsx b/packages/core/src/components/information-bar/test/information-bar.spec.tsx new file mode 100644 index 00000000000..b2e18e017b1 --- /dev/null +++ b/packages/core/src/components/information-bar/test/information-bar.spec.tsx @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { newSpecPage } from '@stencil/core/testing'; +import { InformationBar } from '../information-bar'; + +describe('information-bar', () => { + it('renders', async () => { + const page = await newSpecPage({ + components: [InformationBar], + html: ``, + }); + expect(page.root).toEqualHtml(` + + + + + + + `); + }); +}); diff --git a/packages/core/src/tests/information-bar/basic/index.html b/packages/core/src/tests/information-bar/basic/index.html new file mode 100644 index 00000000000..31b186c588d --- /dev/null +++ b/packages/core/src/tests/information-bar/basic/index.html @@ -0,0 +1,23 @@ + + + + + + + Stencil Component Starter + + + + + + diff --git a/packages/core/src/tests/information-bar/information-bar.e2e.ts b/packages/core/src/tests/information-bar/information-bar.e2e.ts new file mode 100644 index 00000000000..f78338ab834 --- /dev/null +++ b/packages/core/src/tests/information-bar/information-bar.e2e.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { expect } from '@playwright/test'; +import { regressionTest } from '@utils/test'; + +regressionTest.describe('information-bar', () => { + regressionTest('basic', async ({ page }) => { + await page.goto('information-bar/basic'); + + const element = page.locator('ix-information-bar'); + await element.evaluate((element: HTMLIxInformationBarElement) => { + element.bar = [ + { count: 50, stripped: 2, icon: 'alarm', color: 'alarm' }, + { + count: 50, + stripped: 2, + icon: 'warning-rhomb', + color: 'critical', + }, + { count: 5, stripped: 2, icon: 'warning', color: 'warning' }, + { count: 5, stripped: 2, icon: 'info', color: 'info' }, + { count: 5, stripped: 2, icon: 'question', color: 'unassigned' }, + ]; + }); + await page.waitForTimeout(500); + expect(await page.screenshot()).toMatchSnapshot(); + }); +}); diff --git a/packages/core/src/tests/information-bar/information-bar.e2e.ts-snapshots/information-bar-basic-1-chromium---theme-classic-dark-linux.png b/packages/core/src/tests/information-bar/information-bar.e2e.ts-snapshots/information-bar-basic-1-chromium---theme-classic-dark-linux.png new file mode 100644 index 00000000000..20099be8e48 Binary files /dev/null and b/packages/core/src/tests/information-bar/information-bar.e2e.ts-snapshots/information-bar-basic-1-chromium---theme-classic-dark-linux.png differ diff --git a/packages/core/src/tests/information-bar/information-bar.e2e.ts-snapshots/information-bar-basic-1-chromium---theme-classic-light-linux.png b/packages/core/src/tests/information-bar/information-bar.e2e.ts-snapshots/information-bar-basic-1-chromium---theme-classic-light-linux.png new file mode 100644 index 00000000000..a8751a6dac7 Binary files /dev/null and b/packages/core/src/tests/information-bar/information-bar.e2e.ts-snapshots/information-bar-basic-1-chromium---theme-classic-light-linux.png differ diff --git a/packages/react/src/components.ts b/packages/react/src/components.ts index f52595c9013..a1f997bac84 100644 --- a/packages/react/src/components.ts +++ b/packages/react/src/components.ts @@ -48,6 +48,7 @@ export const IxGroupDropdownItem = /*@__PURE__*/createReactComponent('ix-group-item'); export const IxIcon = /*@__PURE__*/createReactComponent('ix-icon'); export const IxIconButton = /*@__PURE__*/createReactComponent('ix-icon-button'); +export const IxInformationBar = /*@__PURE__*/createReactComponent('ix-information-bar'); export const IxInputGroup = /*@__PURE__*/createReactComponent('ix-input-group'); export const IxKeyValue = /*@__PURE__*/createReactComponent('ix-key-value'); export const IxKeyValueList = /*@__PURE__*/createReactComponent('ix-key-value-list'); diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts index 6d500b2e8c6..360277b8422 100644 --- a/packages/vue/src/components.ts +++ b/packages/vue/src/components.ts @@ -376,6 +376,11 @@ export const IxIconButton = /*@__PURE__*/ defineContainer('ix- ]); +export const IxInformationBar = /*@__PURE__*/ defineContainer('ix-information-bar', undefined, [ + 'bar' +]); + + export const IxInputGroup = /*@__PURE__*/ defineContainer('ix-input-group', undefined);