Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ export const Error: StoryObj = {
type="error"
title="Something went wrong"
message="Could not complete the transaction."
buttonText="Dismiss"
buttonText="Dismiss notification"
style="position: static;"
></custom-toast>
`,
}

export const Success: StoryObj = {
export const WalletSyncComplete: StoryObj = {
render: () => html`
<custom-toast
type="success"
title="Success"
message="Activity approved successfully."
buttonText="OK"
title="Wallet sync complete"
message="4 added, 2 updated, 2 disabled."
buttonText="Close notification"
style="position: static;"
></custom-toast>
`,
Expand All @@ -39,8 +39,8 @@ export const Info: StoryObj = {
<custom-toast
type="info"
title="Heads up"
message="Tx has been sent to signing provider."
buttonText="Got it"
message="Transaction has been sent to signing provider."
buttonText="Close notification"
style="position: static;"
></custom-toast>
`,
Expand Down
196 changes: 121 additions & 75 deletions core/wallet-ui-components/src/components/custom-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,101 +7,118 @@ import { BaseElement } from '../internal/base-element'

export type ToastMessageType = 'info' | 'success' | 'error'

const TOAST_DISMISS_DELAY_MS = 5000
const TOAST_ANIMATION_MS = 250

@customElement('custom-toast')
export class Toast extends BaseElement {
static styles = [
BaseElement.styles,
css`
:host {
position: fixed;
bottom: 20px;
right: 20px;
max-width: 500px;
bottom: 20px;
z-index: 1000;
width: min(100vw - 40px, 380px);
font-family: var(--wg-font-family);
}

.toast-wrapper {
--toast-accent-color: var(--wg-error);
--toast-accent-rgb: var(--wg-error-rgb);

position: relative;
display: flex;
flex-direction: column;
border: 2px solid;
padding: 12px 20px;
gap: var(--wg-space-1);
padding: var(--wg-space-4);
padding-right: calc(var(--wg-space-4) + 1.5rem);
background: var(--wg-surface);
color: var(--wg-text);
border: 1px solid rgba(15, 23, 42, 0.08);
border-left: 4px solid var(--toast-accent-color);
border-radius: var(--wg-radius-lg);
font-family: var(--wg-font-family);
}

.info {
background-color: rgba(var(--wg-accent-rgb), 0.1);
border-color: rgba(var(--wg-accent-rgb), 0.3);
box-shadow: var(--wg-shadow-lg);
animation: fadeIn ${TOAST_ANIMATION_MS}ms ease-out;
}

.success {
background-color: rgba(var(--wg-success-rgb), 0.1);
border-color: rgba(var(--wg-success-rgb), 0.3);
.toast-wrapper.info {
--toast-accent-color: var(--wg-accent);
--toast-accent-rgb: var(--wg-accent-rgb);
}

.error {
background-color: rgba(var(--wg-error-rgb), 0.1);
border-color: rgba(var(--wg-error-rgb), 0.3);
.toast-wrapper.success {
--toast-accent-color: var(--wg-success);
--toast-accent-rgb: var(--wg-success-rgb);
}

.toast-title {
font-weight: var(--wg-font-weight-bold);
}

.info .toast-title,
.info .toast-message {
color: var(--wg-accent);
.toast-wrapper.error {
--toast-accent-color: var(--wg-error);
--toast-accent-rgb: var(--wg-error-rgb);
}

.success .toast-title,
.success .toast-message {
color: var(--wg-success);
}

.error .toast-title,
.error .toast-message {
color: var(--wg-error);
.toast-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--wg-space-3);
}

.toast-btn {
.toast-title {
margin: 0;
font-size: var(--wg-font-size-lg);
line-height: var(--wg-line-height-tight);
font-weight: var(--wg-font-weight-bold);
color: var(--toast-accent-color);
}

.toast-message {
margin: 0;
font-size: var(--wg-font-size-base);
line-height: var(--wg-line-height-normal);
color: var(--wg-text-secondary);
}

.toast-close-btn {
position: absolute;
top: 10px;
right: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
padding: 0;
border: none;
border-radius: var(--wg-radius-full);
padding: 0.4rem 0.85rem;
font-size: var(--wg-font-size-sm);
font-weight: var(--wg-font-weight-semibold);
background: transparent;
color: var(--wg-text-secondary);
cursor: pointer;
align-self: flex-end;
transition: background-color 0.2s ease;
transition:
background-color 0.2s ease,
color 0.2s ease;
}

.info .toast-btn {
color: var(--wg-accent);
background: rgba(var(--wg-accent-rgb), 0.15);
}
.info .toast-btn:hover {
background: rgba(var(--wg-accent-rgb), 0.25);
.toast-close-btn:hover {
background: var(--wg-icon-bg);
color: var(--wg-text);
}

.success .toast-btn {
color: var(--wg-success);
background: rgba(var(--wg-success-rgb), 0.15);
}
.success .toast-btn:hover {
background: rgba(var(--wg-success-rgb), 0.25);
.toast-close-btn:focus-visible {
outline: 2px solid var(--toast-accent-color);
outline-offset: 2px;
}

.error .toast-btn {
color: var(--wg-error);
background: rgba(var(--wg-error-rgb), 0.15);
}
.error .toast-btn:hover {
background: rgba(var(--wg-error-rgb), 0.25);
.toast-close-icon {
font-size: 1.1rem;
line-height: 1;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
transform: translateY(8px);
}
to {
opacity: 1;
Expand All @@ -116,64 +133,93 @@ export class Toast extends BaseElement {
}
to {
opacity: 0;
transform: translateY(20px);
transform: translateY(8px);
}
}

.toast-wrapper {
animation: fadeIn 0.6s ease-out;
transition: opacity 0.6s ease-out;
}

.toast-wrapper.closing {
animation: fadeOut 0.6s ease-in forwards;
animation: fadeOut ${TOAST_ANIMATION_MS}ms ease-in forwards;
}

@media (max-width: 600px) {
:host {
max-width: 300px;
right: 16px;
bottom: 16px;
width: calc(100vw - 32px);
}
}
`,
]

@property({ type: String }) title = ''
@property({ type: String }) message = ''
@property({ type: String }) buttonText = 'Okay'
@property({ type: String }) buttonText = 'Dismiss notification'
@property({ type: String }) type: ToastMessageType = 'error'
@property({ type: Boolean }) closing = false

private closeToast() {
private dismissTimeout: number | undefined
private removeTimeout: number | undefined

private closeToast = () => {
if (this.closing) return

if (this.dismissTimeout) {
window.clearTimeout(this.dismissTimeout)
this.dismissTimeout = undefined
}

this.closing = true
setTimeout(() => {
this.removeTimeout = window.setTimeout(() => {
this.closing = false
this.remove()
}, 600)
}, TOAST_ANIMATION_MS)
}

connectedCallback() {
super.connectedCallback()
setTimeout(() => this.closeToast(), 5000)
this.dismissTimeout = window.setTimeout(
() => this.closeToast(),
TOAST_DISMISS_DELAY_MS
)
}

disconnectedCallback(): void {
if (this.dismissTimeout) {
window.clearTimeout(this.dismissTimeout)
this.dismissTimeout = undefined
}

if (this.removeTimeout) {
window.clearTimeout(this.removeTimeout)
this.removeTimeout = undefined
}

super.disconnectedCallback()
}

render() {
const isAssertive = this.type === 'error'

return html`
<div
class="toast-wrapper ${this.type} ${this.closing
? 'closing'
: ''}"
role="alert"
aria-live="assertive"
role=${isAssertive ? 'alert' : 'status'}
aria-live=${isAssertive ? 'assertive' : 'polite'}
aria-atomic="true"
>
<h5 class="toast-title">${this.title}</h5>
<h6 class="toast-message">${this.message}</h6>
<div class="toast-header">
<p class="toast-title">${this.title}</p>
</div>
<p class="toast-message">${this.message}</p>
<button
type="button"
class="toast-btn"
class="toast-close-btn"
aria-label=${this.buttonText}
@click=${this.closeToast}
>
${this.buttonText}
<span class="toast-close-icon" aria-hidden="true">×</span>
</button>
</div>
`
Expand Down
Loading