diff --git a/packages/widget/src/assets/icons/chevron.ts b/packages/widget/src/assets/icons/chevron.ts new file mode 100644 index 00000000..ab4b01bb --- /dev/null +++ b/packages/widget/src/assets/icons/chevron.ts @@ -0,0 +1,19 @@ +import { html } from 'lit'; + +const chevronIcon = html` + + `; + +export default chevronIcon; diff --git a/packages/widget/src/assets/index.ts b/packages/widget/src/assets/index.ts index cb41b5ca..8069197f 100644 --- a/packages/widget/src/assets/index.ts +++ b/packages/widget/src/assets/index.ts @@ -1,9 +1,35 @@ -export { default as switchNetworkIcon } from './icons/switchNetwork'; -export { default as sygmaLogo } from './icons/sygmaLogo'; -export { default as noNetworkIcon } from './icons/noNetworkIcon'; -export { default as ethereumIcon } from './icons/ethereumNetworkIcon'; -export { default as polygonNetworkIcon } from './icons/polygonNetworkIcon'; -export { default as baseNetworkIcon } from './icons/baseNetworkIcon'; -export { default as cronosNetworkIcon } from './icons/cronosNetworkIcon'; -export { default as phalaNetworkIcon } from './icons/phalaNetworkIcon'; -export { default as khalaNetworkIcon } from './icons/khalaNetworkIcon'; +import type { HTMLTemplateResult } from 'lit'; +import baseNetworkIcon from './icons/baseNetworkIcon'; +import cronosNetworkIcon from './icons/cronosNetworkIcon'; +import ethereumIcon from './icons/ethereumNetworkIcon'; +import khalaNetworkIcon from './icons/khalaNetworkIcon'; +import noNetworkIcon from './icons/noNetworkIcon'; +import phalaNetworkIcon from './icons/phalaNetworkIcon'; +import polygonNetworkIcon from './icons/polygonNetworkIcon'; +import switchNetworkIcon from './icons/switchNetwork'; +import sygmaLogo from './icons/sygmaLogo'; +import chevronIcon from './icons/chevron'; + +export const networkIconsMap = { + ethereum: ethereumIcon, + khala: khalaNetworkIcon, + phala: phalaNetworkIcon, + cronos: cronosNetworkIcon, + base: baseNetworkIcon, + gnosis: noNetworkIcon, + polygon: polygonNetworkIcon, + default: noNetworkIcon +} as const as Record; + +export { + ethereumIcon, + khalaNetworkIcon, + phalaNetworkIcon, + cronosNetworkIcon, + baseNetworkIcon, + polygonNetworkIcon, + sygmaLogo, + switchNetworkIcon, + noNetworkIcon, + chevronIcon +}; diff --git a/packages/widget/src/components/network-selector/network-selector.ts b/packages/widget/src/components/network-selector/network-selector.ts index 21f18f42..3cec4355 100644 --- a/packages/widget/src/components/network-selector/network-selector.ts +++ b/packages/widget/src/components/network-selector/network-selector.ts @@ -1,8 +1,11 @@ import type { Domain } from '@buildwithsygma/sygma-sdk-core'; -import { LitElement, html, type HTMLTemplateResult } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { LitElement, html } from 'lit'; +import type { HTMLTemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; import { map } from 'lit/directives/map.js'; +import { when } from 'lit/directives/when.js'; import { capitalize } from '../../utils'; +import { networkIconsMap, chevronIcon } from '../../assets'; import { styles } from './styles'; export const Directions = { @@ -16,70 +19,94 @@ type Direction = (typeof Directions)[keyof typeof Directions]; export class NetworkSelector extends LitElement { static styles = styles; - @property({ - type: Boolean - }) - disabled = false; + @state() + _isDropdownOpen = false; - @property({ - type: Boolean - }) - icons = true; + @property({ type: Boolean }) + disabled = false; - @property({ - type: String - }) + @property({ type: String }) direction?: Direction; - @property({ - type: Object - }) - selected?: Domain; + @property({ type: Object }) + selectedNetwork?: Domain; - @property({ - attribute: false - }) - onNetworkSelected?: (network?: Domain) => void; + @property({ attribute: false }) + onNetworkSelected: (network?: Domain) => void = () => {}; - @property({ - type: Array - }) + @property({ type: Array }) networks: Domain[] = []; - onChange({ target }: Event): void { - const { value } = target as HTMLOptionElement; - const network = this.networks.find((n) => String(n.chainId) == value); - this.onNetworkSelected?.(network); + _toggleDropdown = (): void => { + this._isDropdownOpen = !this._isDropdownOpen; + }; + + _selectOption(option: Domain, event: Event): void { + event.stopPropagation(); + this.selectedNetwork = option; + this._isDropdownOpen = false; + this.onNetworkSelected?.(option); } - renderEntries(): Generator | HTMLTemplateResult { - if (this.networks) { - return map(this.networks, (entry: Domain) => { - // TODO: render network icon - return html``; - }); - } - return html``; + _renderNetworkIcon(name: string): HTMLTemplateResult { + return networkIconsMap[name] || networkIconsMap.default; + } + + _renderEntries(): Generator { + return map( + this.networks, + (network: Domain) => html` + + ` + ); + } + + _renderTriggerContent(): HTMLTemplateResult { + return when( + this.selectedNetwork, + () => + html`${this._renderNetworkIcon(this.selectedNetwork!.name)} + + ${capitalize(this.selectedNetwork!.name)} + `, + () => html`Select Network` + ); } render(): HTMLTemplateResult { - return html`
+ return html`
-
- -
+ ${this._renderEntries()} +
+
`; } } + declare global { interface HTMLElementTagNameMap { 'sygma-network-selector': NetworkSelector; diff --git a/packages/widget/src/components/network-selector/styles.ts b/packages/widget/src/components/network-selector/styles.ts index 845dd49d..1b36041d 100644 --- a/packages/widget/src/components/network-selector/styles.ts +++ b/packages/widget/src/components/network-selector/styles.ts @@ -2,22 +2,24 @@ import { css } from 'lit'; export const styles = css` .selectorContainer { - border-radius: 24px; - border: 1px solid #e4e4e7; + border-radius: 1.5rem; + border: 0.0625rem solid #e4e4e7; display: flex; - width: 314px; - padding: 12px 16px; + width: 100%; + max-width: 19.625rem; + max-height: 4.625rem; + padding: 0.75rem 1rem; flex-direction: column; justify-content: center; align-items: stretch; - gap: 4px; + gap: 0.25rem; } .directionLabel { color: #a1a1aa; - font-size: 14px; + font-size: 0.875rem; font-weight: 500; - line-height: 20px; + line-height: 1.25rem; display: flex; flex-direction: column; justify-content: center; @@ -30,15 +32,89 @@ export const styles = css` .selector { width: 100%; color: #525252; - font-size: 18px; + font-size: 1.125rem; font-weight: 500; - line-height: 26px; + line-height: 1.625rem; border: none; } .selectorSection { display: flex; align-items: center; - gap: 12px; + gap: 0.75rem; + } + + .dropdown { + position: relative; + width: 100%; + } + + .selectedNetwork { + display: flex; + align-items: center; + } + + .dropdownTrigger { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + padding: 0.25rem 0; + box-sizing: border-box; + } + + .chevron { + transform: rotate(0deg); + transition: transform 0.3s ease; + + &.open { + transform: rotate(180deg); + } + } + + .dropdownContent { + display: none; + position: absolute; + background-color: white; + width: 100%; + border-radius: 0.75rem; + border: 0.0625rem solid #f3f4f6; + box-shadow: + 0 0.25rem 0.375rem -0.0625rem rgba(0, 0, 0, 0.1), + 0 0.125rem 0.25rem -0.0625rem rgba(0, 0, 0, 0.06); + z-index: 1; + margin-top: 1rem; + } + + .show { + display: block; + } + + .dropdownOption { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.3s ease; + + svg { + max-width: 1.43656rem; + width: 100%; + } + + &:hover { + background-color: #e9e4dd; + } + } + + .networkIcon { + display: block; + width: 1.25rem; + height: 1.25rem; + } + + .networkName { + margin-left: 0.5rem; } `; diff --git a/packages/widget/src/styles.ts b/packages/widget/src/styles.ts index 3b8120f7..5b009932 100644 --- a/packages/widget/src/styles.ts +++ b/packages/widget/src/styles.ts @@ -28,6 +28,10 @@ export const styles = css` font-family: Inter, sans-serif; } + .networkSelectionWrapper { + margin: 1rem 0 0.5rem 0; + } + .connectAccount { display: flex; justify-content: flex-end; diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index c2e5cd1c..7c481c84 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -22,7 +22,7 @@ class SygmaProtocolWidget extends LitElement {
${switchNetworkIcon} Connect Wallet
-
+
{ - const el = await fixture(html` + + it('renders text when no network selected', async () => { + const el = await fixture(html` `); - assert.equal(el.shadowRoot!.querySelectorAll('.network-option').length, 1); + + const networkOption = el.shadowRoot!.querySelector( + '.selectedNetwork' + ) as HTMLDivElement; + + assert.ok( + networkOption, + 'Network option should be present in the component' + ); + assert.equal( - getDiffableHTML(el.shadowRoot!.querySelector('.selector')!), - getDiffableHTML( - `` - ) + getDiffableHTML(networkOption.textContent?.trim() ?? ''), + getDiffableHTML('Select Network') ); }); + + it('toggles dropdown on click', async () => { + const networks = [ + { id: '1', name: 'ethereum' }, + { id: '2', name: 'khala' } + ]; + + const el = await fixture( + html`` + ); + const dropdownTrigger = el.shadowRoot!.querySelector( + '.dropdownTrigger' + ) as HTMLDivElement; + + assert.ok( + dropdownTrigger, + 'Dropdown trigger should be present in the component' + ); + + // Initial state should be closed + assert.isFalse(el._isDropdownOpen); + + // Simulate click to open dropdown + dropdownTrigger?.click(); + await el.updateComplete; + assert.isTrue(el._isDropdownOpen); + + // Simulate another click to close dropdown + dropdownTrigger?.click(); + await el.updateComplete; + assert.isFalse(el._isDropdownOpen); + }); + it('triggers callback on network selected', async () => { const mockNetworkSelectHandler = vi.fn(); const network = { id: 0, chainId: 1, name: 'Test', type: 'evm' }; - const el = await fixture(html` + const el = await fixture(html` `); - const networkOptions = el.shadowRoot!.querySelectorAll('.network-option'); - assert.equal(networkOptions.length, 2); - const listener = oneEvent( - el.shadowRoot!.querySelector('.selector')!, - 'change', - false - ); - (el.shadowRoot!.querySelector('.selector') as HTMLSelectElement).value = - '1'; - el.shadowRoot!.querySelector('.selector')!.dispatchEvent( - new Event('change') - ); - (networkOptions[1] as HTMLOptionElement).selected = true; - await listener; + + // Simulate selecting a network + const firstOption = el.shadowRoot!.querySelector( + '.dropdownOption' + ) as HTMLDivElement; + assert.ok(firstOption, 'First option should be present in the component'); + firstOption.click(); + await el.updateComplete; + assert.equal(mockNetworkSelectHandler.mock.calls.length, 1); assert.deepEqual(mockNetworkSelectHandler.mock.lastCall, [network]); });