From 11d9ed43151a63aaa2e30b089dbc5a1d226bf343 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 25 Oct 2024 14:45:52 +0800 Subject: [PATCH] feat(elements): add logto-user-password element (#6728) --- .../elements/logto-user-password.test.ts | 68 +++++++++++++++++++ .../account/elements/logto-user-password.ts | 62 +++++++++++++++++ .../elements/src/account/icons/password.svg | 3 + packages/elements/src/account/index.ts | 1 + packages/elements/src/account/react.ts | 7 +- 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 packages/elements/src/account/elements/logto-user-password.test.ts create mode 100644 packages/elements/src/account/elements/logto-user-password.ts create mode 100644 packages/elements/src/account/icons/password.svg diff --git a/packages/elements/src/account/elements/logto-user-password.test.ts b/packages/elements/src/account/elements/logto-user-password.test.ts new file mode 100644 index 000000000..0fd05bc2f --- /dev/null +++ b/packages/elements/src/account/elements/logto-user-password.test.ts @@ -0,0 +1,68 @@ +import { assert, fixture, html, waitUntil } from '@open-wc/testing'; + +import { createMockAccountApi } from '../__mocks__/account-api.js'; +import { type LogtoAccountProvider } from '../providers/logto-account-provider.js'; + +import { LogtoUserPassword } from './logto-user-password.js'; + +suite('logto-user-password', () => { + test('is defined', () => { + const element = document.createElement(LogtoUserPassword.tagName); + assert.instanceOf(element, LogtoUserPassword); + }); + + test('should render error message when account context is not available', async () => { + const element = await fixture( + html`` + ); + await element.updateComplete; + + assert.equal(element.shadowRoot?.textContent, 'Unable to retrieve account context.'); + }); + + test('should render configured status when user has password', async () => { + const mockAccountApi = createMockAccountApi({ + fetchUserProfile: async () => ({ + hasPassword: true, + }), + }); + + const provider = await fixture( + html` + + ` + ); + + await provider.updateComplete; + + const logtoUserPassword = provider.querySelector(LogtoUserPassword.tagName); + + await waitUntil( + () => + logtoUserPassword?.shadowRoot + ?.querySelector('.status') + ?.textContent?.includes('Configured'), + 'Unable to get password status from account context' + ); + }); + + test('should not render status when user has no password', async () => { + const mockAccountApi = createMockAccountApi({ + fetchUserProfile: async () => ({ + hasPassword: false, + }), + }); + + const provider = await fixture( + html` + + ` + ); + + await provider.updateComplete; + const logtoUserPassword = provider.querySelector(LogtoUserPassword.tagName); + + await logtoUserPassword?.updateComplete; + assert.isNull(logtoUserPassword?.shadowRoot?.querySelector('.status')); + }); +}); diff --git a/packages/elements/src/account/elements/logto-user-password.ts b/packages/elements/src/account/elements/logto-user-password.ts new file mode 100644 index 000000000..318738106 --- /dev/null +++ b/packages/elements/src/account/elements/logto-user-password.ts @@ -0,0 +1,62 @@ +import { css, html, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { when } from 'lit/directives/when.js'; + +import passwordIcon from '../icons/password.svg'; + +import { LogtoProfileItemElement } from './LogtoProfileItemElement.js'; + +const tagName = 'logto-user-password'; + +@customElement(tagName) +export class LogtoUserPassword extends LogtoProfileItemElement { + static tagName = tagName; + + static styles = css` + .status { + display: flex; + align-items: center; + gap: var(--logto-spacing-sm); + } + + .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--logto-color-container-on-success); + } + `; + + protected isAccessible(): boolean { + // The password is always accessible + return true; + } + + protected getItemLabelInfo() { + return { + icon: passwordIcon, + label: 'Password', + }; + } + + protected renderContent(): TemplateResult { + const { hasPassword } = this.accountContext?.userProfile ?? {}; + return when( + hasPassword, + () => + html`
+
+ + Configured +
+
` + ); + } +} + +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface HTMLElementTagNameMap { + [tagName]: LogtoUserPassword; + } +} diff --git a/packages/elements/src/account/icons/password.svg b/packages/elements/src/account/icons/password.svg new file mode 100644 index 000000000..355ffafa9 --- /dev/null +++ b/packages/elements/src/account/icons/password.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/elements/src/account/index.ts b/packages/elements/src/account/index.ts index 99e4a3952..38abf4d60 100644 --- a/packages/elements/src/account/index.ts +++ b/packages/elements/src/account/index.ts @@ -7,3 +7,4 @@ export * from './providers/logto-account-provider.js'; export * from './elements/logto-username.js'; export * from './elements/logto-user-email.js'; +export * from './elements/logto-user-password.js'; diff --git a/packages/elements/src/account/react.ts b/packages/elements/src/account/react.ts index b42db8bf8..b4994c819 100644 --- a/packages/elements/src/account/react.ts +++ b/packages/elements/src/account/react.ts @@ -1,7 +1,7 @@ import { createComponent } from '@lit/react'; import { LogtoUsername } from './elements/logto-username.js'; -import { LogtoAccountProvider, LogtoUserEmail } from './index.js'; +import { LogtoAccountProvider, LogtoUserEmail, LogtoUserPassword } from './index.js'; export * from './api/index.js'; @@ -22,5 +22,10 @@ export const createReactComponents = (react: Parameters[ elementClass: LogtoUserEmail, react, }), + LogtoUserPassword: createComponent({ + tagName: LogtoUserPassword.tagName, + elementClass: LogtoUserPassword, + react, + }), }; };