diff --git a/packages/angular-test-app/src/preview-examples/application-header.html b/packages/angular-test-app/src/preview-examples/application-header.html index ee839d7c310..795aafddb25 100644 --- a/packages/angular-test-app/src/preview-examples/application-header.html +++ b/packages/angular-test-app/src/preview-examples/application-header.html @@ -11,9 +11,8 @@ - + - diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index fdf8a8bbf81..8fb8c483597 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -74,14 +74,14 @@ export declare interface IxApplicationHeader extends Components.IxApplicationHea @ProxyCmp({ - inputs: ['image', 'initials'] + inputs: ['extra', 'image', 'initials', 'username'] }) @Component({ selector: 'ix-avatar', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['image', 'initials'], + inputs: ['extra', 'image', 'initials', 'username'], }) export class IxAvatar { protected el: HTMLElement; diff --git a/packages/core/component-doc.json b/packages/core/component-doc.json index e21a92a8c90..df7f100098c 100644 --- a/packages/core/component-doc.json +++ b/packages/core/component-doc.json @@ -481,11 +481,15 @@ ], "dependencies": [ "ix-dropdown", + "ix-divider", + "ix-typography", "ix-spinner" ], "dependencyGraph": { "ix-avatar": [ "ix-dropdown", + "ix-divider", + "ix-typography", "ix-spinner" ], "ix-menu-avatar": [ @@ -493,6 +497,32 @@ ] }, "props": [ + { + "name": "extra", + "type": "string", + "complexType": { + "original": "string", + "resolved": "string", + "references": {} + }, + "mutable": false, + "attr": "extra", + "reflectToAttr": false, + "docs": "Optional description text that will be displayed underneath the username.\nNote: Only working if avatar is part of the ix-application-header", + "docsTags": [ + { + "name": "since", + "text": "2.1.0" + } + ], + "values": [ + { + "type": "string" + } + ], + "optional": false, + "required": false + }, { "name": "image", "type": "string", @@ -534,6 +564,32 @@ ], "optional": false, "required": false + }, + { + "name": "username", + "type": "string", + "complexType": { + "original": "string", + "resolved": "string", + "references": {} + }, + "mutable": false, + "attr": "username", + "reflectToAttr": false, + "docs": "If set an info card displaying the username will be placed inside the dropdown.\nNote: Only working if avatar is part of the ix-application-header", + "docsTags": [ + { + "name": "since", + "text": "2.1.0" + } + ], + "values": [ + { + "type": "string" + } + ], + "optional": false, + "required": false } ], "methods": [], @@ -4621,10 +4677,14 @@ ], "encapsulation": "shadow", "dependents": [ + "ix-avatar", "ix-menu-category" ], "dependencies": [], "dependencyGraph": { + "ix-avatar": [ + "ix-divider" + ], "ix-menu-category": [ "ix-divider" ] @@ -9903,6 +9963,8 @@ ], "ix-avatar": [ "ix-dropdown", + "ix-divider", + "ix-typography", "ix-spinner" ], "ix-menu-avatar-item": [ @@ -15684,6 +15746,7 @@ "dependents": [ "ix-action-card", "ix-application-switch-modal", + "ix-avatar", "ix-blind", "ix-card-list", "ix-content-header", @@ -15706,6 +15769,9 @@ "ix-application-switch-modal": [ "ix-typography" ], + "ix-avatar": [ + "ix-typography" + ], "ix-blind": [ "ix-typography" ], diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index f696e059de8..7a13144f24a 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -145,6 +145,11 @@ export namespace Components { * @since 2.0.0 */ interface IxAvatar { + /** + * Optional description text that will be displayed underneath the username. Note: Only working if avatar is part of the ix-application-header + * @since 2.1.0 + */ + "extra": string; /** * Display a avatar image */ @@ -153,6 +158,11 @@ export namespace Components { * Display the initials of the user. Will be overwritten by image */ "initials": string; + /** + * If set an info card displaying the username will be placed inside the dropdown. Note: Only working if avatar is part of the ix-application-header + * @since 2.1.0 + */ + "username": string; } /** * @deprecated ix-basic-navigation is deprecated in favor of ix-application @@ -4016,6 +4026,11 @@ declare namespace LocalJSX { * @since 2.0.0 */ interface IxAvatar { + /** + * Optional description text that will be displayed underneath the username. Note: Only working if avatar is part of the ix-application-header + * @since 2.1.0 + */ + "extra"?: string; /** * Display a avatar image */ @@ -4024,6 +4039,11 @@ declare namespace LocalJSX { * Display the initials of the user. Will be overwritten by image */ "initials"?: string; + /** + * If set an info card displaying the username will be placed inside the dropdown. Note: Only working if avatar is part of the ix-application-header + * @since 2.1.0 + */ + "username"?: string; } /** * @deprecated ix-basic-navigation is deprecated in favor of ix-application diff --git a/packages/core/src/components/avatar/avatar.scss b/packages/core/src/components/avatar/avatar.scss index 73b2c7901ad..bf1d2890288 100644 --- a/packages/core/src/components/avatar/avatar.scss +++ b/packages/core/src/components/avatar/avatar.scss @@ -39,6 +39,7 @@ min-width: 2rem; border-radius: 100px; background-color: var(--theme-color-component-3); + color: var(--theme-color-std-text); } #avatar-path-background { @@ -51,6 +52,48 @@ } } +:host { + .user-info { + display: flex; + flex-direction: row; + position: relative; + height: 2.5rem; + padding: 1rem; + width: 12.75rem; + min-width: 12.75rem; + max-width: 12.75rem; + + gap: 1rem; + + .avatar { + width: 2rem; + pointer-events: none; + } + + .user { + display: flex; + position: relative; + flex-direction: column; + justify-content: center; + max-width: 10rem; + width: 100%; + overflow: hidden; + } + + .username { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .extra { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } +} + :host(.avatar-button) { button { @include btn-base; diff --git a/packages/core/src/components/avatar/avatar.tsx b/packages/core/src/components/avatar/avatar.tsx index 49ad5f98647..9ee7516e460 100644 --- a/packages/core/src/components/avatar/avatar.tsx +++ b/packages/core/src/components/avatar/avatar.tsx @@ -7,6 +7,19 @@ * LICENSE file in the root directory of this source tree. */ +import { + Component, + Element, + Fragment, + h, + Host, + Prop, + readTask, + State, +} from '@stencil/core'; +import { BaseButton } from '../button/base-button'; +import { closestElement, hasSlottedElements } from '../utils/shadow-dom'; + function DefaultAvatar(props: { initials?: string }) { const { initials } = props; @@ -40,17 +53,40 @@ function DefaultAvatar(props: { initials?: string }) { ); } -import { - Component, - Element, - h, - Host, - Prop, - readTask, - State, -} from '@stencil/core'; -import { BaseButton } from '../button/base-button'; -import { closestElement } from '../utils/shadow-dom'; +function AvatarImage(props: { image: string; initials: string }) { + return ( + + {props.image ? ( + + ) : ( + + )} + + ); +} + +function UserInfo(props: { + image: string; + initials: string; + userName: string; + extra: string; +}) { + return ( + + event.preventDefault()}> + + + {props.userName} + {props.extra && ( + + {props.extra} + + )} + + + + ); +} /** * @since 2.0.0 @@ -75,13 +111,36 @@ export class Avatar { */ @Prop() initials: string; + /** + * If set an info card displaying the username will be placed inside the dropdown. + * Note: Only working if avatar is part of the ix-application-header + * + * @since 2.1.0 + */ + @Prop() username: string; + + /** + * Optional description text that will be displayed underneath the username. + * Note: Only working if avatar is part of the ix-application-header + * + * @since 2.1.0 + */ + @Prop() extra: string; + @State() isClosestApplicationHeader = false; + @State() hasSlottedElements = false; + + private slotElement: HTMLSlotElement; componentWillLoad() { const closest = closestElement('ix-application-header', this.hostElement); this.isClosestApplicationHeader = closest !== null; } + private slottedChanged() { + this.hasSlottedElements = hasSlottedElements(this.slotElement); + } + private resolveAvatarTrigger() { return new Promise((resolve) => { readTask(() => @@ -106,16 +165,27 @@ export class Avatar { type="button" variant="primary" > - - {this.image ? ( - - ) : ( - - )} - + - - + + {this.username && ( + + + {this.hasSlottedElements && } + + )} + this.slottedChanged()} + ref={(ref: HTMLSlotElement) => (this.slotElement = ref)} + > ); @@ -123,13 +193,7 @@ export class Avatar { return ( - - {this.image ? ( - - ) : ( - - )} - + ); } diff --git a/packages/core/src/components/avatar/test/avatar.ct.ts b/packages/core/src/components/avatar/test/avatar.ct.ts index 922304b6fd7..806d47b04e6 100644 --- a/packages/core/src/components/avatar/test/avatar.ct.ts +++ b/packages/core/src/components/avatar/test/avatar.ct.ts @@ -44,4 +44,75 @@ test.describe('embedded into header', () => { await expect(avatar.locator('ix-dropdown')).toHaveClass(/show/); await expect(avatar.getByText('Item 1')).toBeVisible(); }); + + test('show user-info', async ({ page, mount }) => { + await page.setViewportSize(viewPorts.lg); + await mount( + ` + + + + + ` + ); + + const avatar = page.locator('ix-avatar'); + await avatar.click(); + + const userInfo = avatar.locator('.user-info'); + const username = userInfo.locator('.username'); + const extra = userInfo.locator('.extra'); + + await expect(avatar.locator('.user-info')).toBeVisible(); + + await expect(username).toHaveText('foo'); + await expect(extra).toHaveText('bar'); + + await expect(avatar.locator('ix-divider')).not.toBeVisible(); + }); + + test('should show divider if a element is slotted', async ({ + page, + mount, + }) => { + await page.setViewportSize(viewPorts.lg); + await mount( + ` + + + test + + + ` + ); + + const avatar = page.locator('ix-avatar'); + await avatar.click(); + + await expect(avatar.locator('ix-divider')).toBeVisible(); + }); + + test('should hide user info if no username is provided', async ({ + page, + mount, + }) => { + await page.setViewportSize(viewPorts.lg); + await mount( + ` + + + Test + + + ` + ); + + const avatar = page.locator('ix-avatar'); + await avatar.click(); + + const userInfo = avatar.locator('.user-info'); + + await expect(userInfo).not.toBeVisible(); + await expect(avatar.locator('ix-divider')).not.toBeVisible(); + }); }); diff --git a/packages/core/src/components/divider/divider.scss b/packages/core/src/components/divider/divider.scss index 605a0ae59b3..7d548110e8d 100644 --- a/packages/core/src/components/divider/divider.scss +++ b/packages/core/src/components/divider/divider.scss @@ -12,6 +12,6 @@ position: relative; width: 100%; - border: 0.0625rem solid var(--theme-color-x-weak-bdr); + border-bottom: 0.0625rem solid var(--theme-color-x-weak-bdr); margin: 0.25rem 0px; } diff --git a/packages/core/src/tests/avatar/avatar.e2e.ts b/packages/core/src/tests/avatar/avatar.e2e.ts index aada13fc565..6d867520b56 100644 --- a/packages/core/src/tests/avatar/avatar.e2e.ts +++ b/packages/core/src/tests/avatar/avatar.e2e.ts @@ -13,16 +13,26 @@ import { regressionTest } from '@utils/test'; regressionTest.describe('avatar', () => { regressionTest('basic', async ({ page }) => { await page.goto('avatar/basic'); - expect(await page.screenshot({ fullPage: true })).toMatchSnapshot(); + await expect(page).toHaveScreenshot({ fullPage: true }); }); regressionTest('image', async ({ page }) => { await page.goto('avatar/image'); - expect(await page.screenshot({ fullPage: true })).toMatchSnapshot(); + await expect(page).toHaveScreenshot({ fullPage: true }); }); regressionTest('initials', async ({ page }) => { await page.goto('avatar/initials'); - expect(await page.screenshot({ fullPage: true })).toMatchSnapshot(); + await expect(page).toHaveScreenshot({ fullPage: true }); + }); + + regressionTest('user-info', async ({ page }) => { + await page.goto('avatar/user-info'); + + const avatar = page.locator('ix-avatar'); + + await avatar.click(); + + await expect(page).toHaveScreenshot({ fullPage: true }); }); }); diff --git a/packages/core/src/tests/avatar/avatar.e2e.ts-snapshots/avatar-user-info-1-chromium---theme-classic-dark-linux.png b/packages/core/src/tests/avatar/avatar.e2e.ts-snapshots/avatar-user-info-1-chromium---theme-classic-dark-linux.png new file mode 100644 index 00000000000..a9629d67e98 Binary files /dev/null and b/packages/core/src/tests/avatar/avatar.e2e.ts-snapshots/avatar-user-info-1-chromium---theme-classic-dark-linux.png differ diff --git a/packages/core/src/tests/avatar/avatar.e2e.ts-snapshots/avatar-user-info-1-chromium---theme-classic-light-linux.png b/packages/core/src/tests/avatar/avatar.e2e.ts-snapshots/avatar-user-info-1-chromium---theme-classic-light-linux.png new file mode 100644 index 00000000000..92267b40482 Binary files /dev/null and b/packages/core/src/tests/avatar/avatar.e2e.ts-snapshots/avatar-user-info-1-chromium---theme-classic-light-linux.png differ diff --git a/packages/core/src/tests/avatar/user-info/index.html b/packages/core/src/tests/avatar/user-info/index.html new file mode 100644 index 00000000000..58093809b50 --- /dev/null +++ b/packages/core/src/tests/avatar/user-info/index.html @@ -0,0 +1,24 @@ + + + + + + + + Stencil Component Starter + + + + + + Test + + + + + + diff --git a/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-dark-linux.png b/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-dark-linux.png index a8d65d6d918..8e7f380527d 100644 Binary files a/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-dark-linux.png and b/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-dark-linux.png differ diff --git a/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-light-linux.png b/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-light-linux.png index 748ad03875b..b7a2e63a663 100644 Binary files a/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-light-linux.png and b/packages/core/src/tests/dropdown-quick-actions/dropdown-quick-actions.e2e.ts-snapshots/dropdown-quick-actions-basic-1-chromium---theme-classic-light-linux.png differ diff --git a/packages/documentation/docs/controls/_avatar_code.md b/packages/documentation/docs/controls/_avatar_code.md index bddc55e2c58..a2e6c7fd829 100644 --- a/packages/documentation/docs/controls/_avatar_code.md +++ b/packages/documentation/docs/controls/_avatar_code.md @@ -63,7 +63,7 @@ frameworks={{ You can also add the avatar to the header which will turn it into a clickable button. - + ## Properties diff --git a/packages/documentation/docs/controls/application-frame/application-header.md b/packages/documentation/docs/controls/application-frame/application-header.md index 09fb3420fcc..a3225428a46 100644 --- a/packages/documentation/docs/controls/application-frame/application-header.md +++ b/packages/documentation/docs/controls/application-frame/application-header.md @@ -15,7 +15,7 @@ The ix-application-header can host custom content which will be displayed on the ## Usage - + ### Avatar diff --git a/packages/html-test-app/src/preview-examples/application-header.html b/packages/html-test-app/src/preview-examples/application-header.html index f9e7d9ce342..a89bc92cb47 100644 --- a/packages/html-test-app/src/preview-examples/application-header.html +++ b/packages/html-test-app/src/preview-examples/application-header.html @@ -21,10 +21,9 @@ - + - diff --git a/packages/react-test-app/src/preview-examples/application-header.tsx b/packages/react-test-app/src/preview-examples/application-header.tsx index 2055cad6890..958ca4a9726 100644 --- a/packages/react-test-app/src/preview-examples/application-header.tsx +++ b/packages/react-test-app/src/preview-examples/application-header.tsx @@ -31,10 +31,9 @@ export default () => { - + - ); diff --git a/packages/vue-test-app/src/preview-examples/application-header.vue b/packages/vue-test-app/src/preview-examples/application-header.vue index dd85887d649..910f14d547f 100644 --- a/packages/vue-test-app/src/preview-examples/application-header.vue +++ b/packages/vue-test-app/src/preview-examples/application-header.vue @@ -31,10 +31,9 @@ import { - + - diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts index 689c4d85473..055f5f0cd90 100644 --- a/packages/vue/src/components.ts +++ b/packages/vue/src/components.ts @@ -124,7 +124,9 @@ export const IxApplicationHeader = /*@__PURE__*/ defineContainer('ix-avatar', defineIxAvatar, [ 'image', - 'initials' + 'initials', + 'username', + 'extra' ]);