mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(elements): add logto-social-identity element (#6736)
This commit is contained in:
parent
3d25448500
commit
8f59a20c22
6 changed files with 249 additions and 0 deletions
|
@ -0,0 +1,88 @@
|
|||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
import fallbackAvatar from '../icons/fallback-avatar.svg';
|
||||
|
||||
const tagName = 'logto-identity-info';
|
||||
|
||||
@customElement(tagName)
|
||||
export class LogtoIdentityInfo extends LitElement {
|
||||
static tagName = tagName;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--logto-spacing-sm);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
--logto-icon-size: var(--logto-identity-info-avatar-size, 36px);
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: var(--logto-identity-info-avatar-size, 36px);
|
||||
height: var(--logto-identity-info-avatar-size, 36px);
|
||||
border-radius: var(--logto-identity-info-avatar-shape, var(--logto-shape-corner-md));
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.name {
|
||||
font: var(--logto-identity-info-name-font-size, var(--logto-font-body-md));
|
||||
color: var(
|
||||
--logto-identity-info-name-color,
|
||||
var(--logto-color---logto-color-typeface-primary)
|
||||
);
|
||||
}
|
||||
|
||||
.email {
|
||||
font: var(--logto-identity-info-email-font, var(--logto-font-body-sm));
|
||||
color: var(
|
||||
--logto-identity-info-email-color,
|
||||
var(--logto-color---logto-color-typeface-primary)
|
||||
);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
avatar = '';
|
||||
|
||||
@property({ type: String })
|
||||
name = '';
|
||||
|
||||
@property({ type: String })
|
||||
email = '';
|
||||
|
||||
@state()
|
||||
failedToLoadAvatar = false;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="avatar">
|
||||
${this.avatar && !this.failedToLoadAvatar
|
||||
? html`<img src="${this.avatar}" alt="user avatar" @error=${this.handleAvatarError} />`
|
||||
: html`<logto-icon>${fallbackAvatar}</logto-icon>`}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="name">${this.name}</div>
|
||||
<div class="email">${this.email}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleAvatarError() {
|
||||
this.failedToLoadAvatar = true;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface HTMLElementTagNameMap {
|
||||
[tagName]: LogtoIdentityInfo;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { assert, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
|
||||
import { createMockAccountApi } from '../__mocks__/account-api.js';
|
||||
import { LogtoIdentityInfo } from '../components/logto-identity-info.js';
|
||||
import { type LogtoAccountProvider } from '../providers/logto-account-provider.js';
|
||||
|
||||
import { LogtoSocialIdentity } from './logto-social-identity.js';
|
||||
|
||||
suite('logto-social-identity', () => {
|
||||
test('is defined', () => {
|
||||
const element = document.createElement(LogtoSocialIdentity.tagName);
|
||||
assert.instanceOf(element, LogtoSocialIdentity);
|
||||
});
|
||||
|
||||
test('should render error message when account context is not available', async () => {
|
||||
const element = await fixture<LogtoSocialIdentity>(
|
||||
html`<logto-social-identity></logto-social-identity>`
|
||||
);
|
||||
await element.updateComplete;
|
||||
|
||||
assert.equal(element.shadowRoot?.textContent, 'Unable to retrieve account context.');
|
||||
});
|
||||
|
||||
test('should render correctly when user has permission to view social identity information', async () => {
|
||||
const mockAccountApi = createMockAccountApi({
|
||||
fetchUserProfile: async () => ({
|
||||
identities: {
|
||||
github: {
|
||||
userId: '123',
|
||||
details: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const provider = await fixture<LogtoAccountProvider>(
|
||||
html`<logto-account-provider .accountApi=${mockAccountApi}>
|
||||
<logto-social-identity target="github"></logto-social-identity>
|
||||
</logto-account-provider>`
|
||||
);
|
||||
|
||||
await provider.updateComplete;
|
||||
|
||||
const logtoSocialIdentity = provider.querySelector<LogtoSocialIdentity>(
|
||||
LogtoSocialIdentity.tagName
|
||||
);
|
||||
|
||||
const identityInfo = logtoSocialIdentity?.shadowRoot?.querySelector<LogtoIdentityInfo>(
|
||||
LogtoIdentityInfo.tagName
|
||||
);
|
||||
|
||||
await waitUntil(
|
||||
() =>
|
||||
identityInfo?.shadowRoot?.querySelector('div[class=name]')?.textContent === 'John Doe' &&
|
||||
identityInfo.shadowRoot.querySelector('div[class=email]')?.textContent ===
|
||||
'john@example.com',
|
||||
'Unable to get social identity information from account context'
|
||||
);
|
||||
});
|
||||
|
||||
test('should render nothing if the user lacks permission to view social identity information', async () => {
|
||||
const mockAccountApi = createMockAccountApi({
|
||||
fetchUserProfile: async () => ({
|
||||
identities: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
const provider = await fixture<LogtoAccountProvider>(
|
||||
html`<logto-account-provider .accountApi=${mockAccountApi}>
|
||||
<logto-social-identity target="github" labelText="GitHub"></logto-social-identity>
|
||||
</logto-account-provider>`
|
||||
);
|
||||
|
||||
await provider.updateComplete;
|
||||
const logtoSocialIdentity = provider.querySelector<LogtoSocialIdentity>(
|
||||
LogtoSocialIdentity.tagName
|
||||
);
|
||||
|
||||
await logtoSocialIdentity?.updateComplete;
|
||||
assert.equal(logtoSocialIdentity?.shadowRoot?.children.length, 0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
import { html, type TemplateResult } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
import usernameIcon from '../icons/username.svg';
|
||||
|
||||
import { LogtoProfileItemElement } from './LogtoProfileItemElement.js';
|
||||
|
||||
const tagName = 'logto-social-identity';
|
||||
|
||||
@customElement(tagName)
|
||||
export class LogtoSocialIdentity extends LogtoProfileItemElement {
|
||||
static tagName = tagName;
|
||||
|
||||
@property({ type: String })
|
||||
target = '';
|
||||
|
||||
protected isAccessible(): boolean {
|
||||
return this.accountContext?.userProfile.identities !== undefined;
|
||||
}
|
||||
|
||||
protected getItemLabelInfo() {
|
||||
// Todo: @xiaoyijun replace with correct label text and icon when related connector API is ready
|
||||
return {
|
||||
icon: usernameIcon,
|
||||
label: this.target,
|
||||
};
|
||||
}
|
||||
|
||||
protected renderContent(): TemplateResult {
|
||||
const { identities } = this.accountContext?.userProfile ?? {};
|
||||
|
||||
const identity = identities?.[this.target];
|
||||
// Todo: @xiaoyijun support identifier fallback logic
|
||||
const { avatar = '', name = '', email = '' } = identity?.details ?? {};
|
||||
|
||||
return when(
|
||||
identity,
|
||||
() =>
|
||||
html`<logto-identity-info
|
||||
slot="content"
|
||||
.avatar=${String(avatar)}
|
||||
.name=${String(name)}
|
||||
.email=${String(email)}
|
||||
></logto-identity-info>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface HTMLElementTagNameMap {
|
||||
[tagName]: LogtoSocialIdentity;
|
||||
}
|
||||
}
|
13
packages/elements/src/account/icons/fallback-avatar.svg
Normal file
13
packages/elements/src/account/icons/fallback-avatar.svg
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_969_33139)">
|
||||
<path d="M34 0H6C2.68629 0 0 2.68629 0 6V34C0 37.3137 2.68629 40 6 40H34C37.3137 40 40 37.3137 40 34V6C40 2.68629 37.3137 0 34 0Z" fill="#F7F8F8"/>
|
||||
<path d="M34 0H6C2.68629 0 0 2.68629 0 6V34C0 37.3137 2.68629 40 6 40H34C37.3137 40 40 37.3137 40 34V6C40 2.68629 37.3137 0 34 0Z" fill="#78767F" fill-opacity="0.02"/>
|
||||
<path d="M34 0H6C2.68629 0 0 2.68629 0 6V34C0 37.3137 2.68629 40 6 40H34C37.3137 40 40 37.3137 40 34V6C40 2.68629 37.3137 0 34 0Z" fill="#5D34F2" fill-opacity="0.16"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1449 8.3335C17.6236 8.3335 15.5796 10.3774 15.5796 12.8987V13.9132C15.5796 16.4345 17.6236 18.4784 20.1449 18.4784C22.6662 18.4784 24.7101 16.4345 24.7101 13.9132V12.8987C24.7101 10.3774 22.6662 8.3335 20.1449 8.3335ZM20.1449 31.6668C30.2899 31.6668 30.2899 28.369 30.2899 28.369C30.2899 24.0916 28.1669 20.1045 22.6812 20.0002C22.6579 19.9997 22.4995 20.1538 22.2699 20.3772C21.6671 20.9635 20.5736 22.0272 20.1449 22.0272C19.7162 22.0272 18.6227 20.9635 18.02 20.3772C17.7904 20.1538 17.6319 19.9997 17.6087 20.0002C12.123 20.1045 10 24.0916 10 28.369C10 28.369 10 31.6668 20.1449 31.6668Z" fill="#947DFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_969_33139">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -2,6 +2,7 @@ export * from './api/index.js';
|
|||
|
||||
export * from './components/logto-icon.js';
|
||||
export * from './components/logto-profile-item.js';
|
||||
export * from './components/logto-identity-info.js';
|
||||
|
||||
export * from './providers/logto-account-provider.js';
|
||||
|
||||
|
@ -9,3 +10,4 @@ export * from './elements/logto-username.js';
|
|||
export * from './elements/logto-user-email.js';
|
||||
export * from './elements/logto-user-password.js';
|
||||
export * from './elements/logto-user-phone.js';
|
||||
export * from './elements/logto-social-identity.js';
|
||||
|
|
|
@ -3,6 +3,7 @@ import { createComponent } from '@lit/react';
|
|||
import { LogtoUsername } from './elements/logto-username.js';
|
||||
import {
|
||||
LogtoAccountProvider,
|
||||
LogtoSocialIdentity,
|
||||
LogtoUserEmail,
|
||||
LogtoUserPassword,
|
||||
LogtoUserPhone,
|
||||
|
@ -37,5 +38,10 @@ export const createReactComponents = (react: Parameters<typeof createComponent>[
|
|||
elementClass: LogtoUserPhone,
|
||||
react,
|
||||
}),
|
||||
LogtoSocialIdentity: createComponent({
|
||||
tagName: LogtoSocialIdentity.tagName,
|
||||
elementClass: LogtoSocialIdentity,
|
||||
react,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue