Skip to content

Commit a7e295c

Browse files
committed
refactor(components/widget): extract amount component
1 parent bab42f7 commit a7e295c

4 files changed

Lines changed: 296 additions & 206 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
:host {
2+
font-family: var(--wm-font-family, system-ui, sans-serif);
3+
display: block;
4+
flex: 0;
5+
6+
--primary-color: var(--wm-primary-color, #56b7b5);
7+
--background-color: var(--wm-background-color, #ffffff);
8+
--text-color: var(--wm-text-color, #363636);
9+
}
10+
11+
fieldset {
12+
border: 0;
13+
padding: 0;
14+
margin: 0;
15+
width: 100%;
16+
}
17+
18+
.preset-buttons {
19+
display: flex;
20+
justify-content: center;
21+
gap: 12px;
22+
margin: var(--Spacings-lg, 24px) 0 var(--Spacings-lg, 24px) 0;
23+
}
24+
25+
.preset-buttons button {
26+
display: flex;
27+
width: calc(var(--Font-Size-text-base) + 2.75rem);
28+
height: calc(var(--Font-Size-text-base) + 2.75rem);
29+
cursor: pointer;
30+
padding: 16px 12px;
31+
justify-content: center;
32+
align-items: center;
33+
gap: 10px;
34+
border-radius: 100px;
35+
border: 1px solid var(--text-color, #c9c9c9);
36+
37+
background: var(--background-color, #f2fbf9);
38+
color: var(--Text-paragraph-standard, #7b7b7b);
39+
font-family: var(--Font-Family-Inter, Inter);
40+
font-size: var(--Font-Size-text-base, 16px);
41+
font-style: normal;
42+
font-weight: var(--Font-Weight-Bold, 700);
43+
line-height: var(--Font-Line-Height-md, 24px);
44+
}
45+
46+
.preset-buttons button:hover {
47+
background: #0000002f;
48+
}
49+
50+
.preset-buttons [aria-checked='true'] {
51+
background: color-mix(
52+
in srgb,
53+
var(--primary-color, #f2fbf9) 45%,
54+
transparent
55+
);
56+
border: 1px solid var(--primary-color, #5b5380);
57+
}
58+
59+
.currency-symbol {
60+
position: absolute;
61+
left: var(--Spacings-md, 12px);
62+
top: 50%;
63+
transform: translateY(-50%);
64+
color: var(--Text-paragraph-standard, #363636);
65+
font-family: var(--Font-Family-Inter, Inter);
66+
font-size: var(--Font-Size-text-base, 16px);
67+
font-weight: var(--Font-Weight-Regular, 400);
68+
pointer-events: none;
69+
z-index: 1;
70+
}
71+
72+
.form-input {
73+
display: flex;
74+
flex-direction: column;
75+
align-items: flex-start;
76+
gap: 4px;
77+
align-self: stretch;
78+
}
79+
80+
.form-input[aria-invalid='true'] {
81+
border-color: var(--Colors-red-500, #ef4444);
82+
}
83+
84+
.form-label {
85+
display: flex;
86+
justify-content: flex-start;
87+
align-items: center;
88+
gap: 2px;
89+
color: var(--Text-paragraph-standard, #363636);
90+
font-family: var(--Font-Family-Inter, Inter);
91+
font-size: var(--Font-Size-text-xs, 12px);
92+
font-style: normal;
93+
font-weight: var(--Font-Weight-Regular, 400);
94+
line-height: var(--Font-Line-Height-xs, 16px);
95+
}
96+
97+
.amount-input-wrapper {
98+
position: relative;
99+
width: 100%;
100+
margin-top: 4px;
101+
}
102+
103+
.form-input.with-currency {
104+
padding-left: calc(var(--Font-Size-text-base) + 2rem);
105+
border-width: 2px;
106+
}
107+
108+
.amount-error {
109+
color: var(--Colors-red-500, #ef4444);
110+
font-family: var(--Font-Family-Inter, Inter);
111+
font-size: var(--Font-Size-text-sm, 14px);
112+
font-style: normal;
113+
font-weight: var(--Font-Weight-Regular, 400);
114+
line-height: var(--Font-Line-Height-sm, 20px);
115+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { LitElement, html, nothing, unsafeCSS } from 'lit'
2+
import { property, query, state } from 'lit/decorators.js'
3+
import { getCurrencySymbol } from '@c/utils'
4+
import styles from './amount.css?raw'
5+
import stylesBase from '../confirmation/confirmation.css?raw'
6+
7+
export interface AmountChangeEventDetail {
8+
amount: number
9+
}
10+
11+
export class PaymentAmount extends LitElement {
12+
#debounceTimer: ReturnType<typeof setTimeout> | null = null
13+
14+
@property({ type: String }) currency = 'USD'
15+
@property({ type: Number }) minSendAmount = 0.5
16+
@property({ type: Array }) presets: number[] = [1, 5, 10]
17+
@property({ type: Number }) value = 0
18+
@property({ type: String }) externalError = ''
19+
20+
@query('input') private _inputEl!: HTMLInputElement
21+
22+
@state() private _internalError: string | null = null
23+
24+
static styles = [unsafeCSS(stylesBase), unsafeCSS(styles)]
25+
26+
connectedCallback(): void {
27+
super.connectedCallback()
28+
}
29+
30+
protected firstUpdated() {
31+
this._inputEl.focus()
32+
}
33+
34+
private _handleInput(e: Event) {
35+
const el = e.target as HTMLInputElement
36+
const rawValue = el.value
37+
38+
// assumes assetScale=2 here
39+
const validFormat = /^\d*\.?\d{0,2}$/.test(rawValue)
40+
if (!validFormat) {
41+
el.value = this.value === 0 ? '' : this.value.toString()
42+
return
43+
}
44+
45+
const newValue = Number.parseFloat(rawValue) || 0
46+
if (newValue === this.value) return
47+
if (Number.isNaN(newValue)) return
48+
49+
this.value = newValue
50+
51+
if (newValue <= 0 || newValue < this.minSendAmount) {
52+
if (this.minSendAmount) {
53+
this._internalError = `Please enter an amount greater than ${getCurrencySymbol(this.currency)}${this.minSendAmount}`
54+
} else {
55+
this._internalError = `Please enter a higher amount.`
56+
}
57+
return
58+
}
59+
60+
this._internalError = null
61+
this._processUpdate()
62+
}
63+
64+
private _handlePresetClick(presetAmount: number) {
65+
this._internalError = null
66+
this.value = presetAmount
67+
this._processUpdate(true)
68+
}
69+
70+
private _processUpdate(immediate = false) {
71+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer)
72+
this.#debounceTimer = setTimeout(
73+
() => this.onChange(this.value),
74+
immediate ? 0 : 750,
75+
)
76+
}
77+
78+
private onChange(amount: number) {
79+
this.dispatchEvent(
80+
new CustomEvent<AmountChangeEventDetail>('change', {
81+
detail: { amount },
82+
}),
83+
)
84+
}
85+
86+
render() {
87+
const currencySymbol = getCurrencySymbol(this.currency)
88+
const displayError = this.externalError || this._internalError
89+
const hasError = !!displayError
90+
91+
return html`
92+
<fieldset>
93+
<legend id="amount-label" class="form-label">Amount</legend>
94+
95+
<div class="amount-input-wrapper">
96+
<span class="currency-symbol">${currencySymbol}</span>
97+
<input
98+
id="amount-input"
99+
aria-labelledby="amount-label"
100+
class="form-input with-currency"
101+
type="text"
102+
inputmode="decimal"
103+
placeholder="1.5"
104+
.value=${this.value === 0 ? '' : this.value.toString()}
105+
@input=${this._handleInput}
106+
@keydown=${allowOnlyNumericInput}
107+
aria-invalid=${hasError}
108+
aria-describedby=${hasError ? 'amount-error' : nothing}
109+
@paste=${(ev: Event) => ev.preventDefault()}
110+
autocomplete="off"
111+
spellcheck="false"
112+
/>
113+
</div>
114+
<p
115+
id="amount-error"
116+
class="amount-error"
117+
role="alert"
118+
>
119+
${hasError ? displayError : nothing}
120+
</p>
121+
</div>
122+
123+
<div class="preset-buttons" role="radiogroup" aria-label="Preset amounts">
124+
${this.presets.map(
125+
(amount) => html`
126+
<button
127+
type="button"
128+
role="radio"
129+
aria-checked="${this.value === amount}"
130+
@click=${() => this._handlePresetClick(amount)}
131+
>
132+
${currencySymbol}${amount}
133+
</button>
134+
`,
135+
)}
136+
</fieldset>
137+
`
138+
}
139+
}
140+
141+
function allowOnlyNumericInput(
142+
ev: KeyboardEvent & { currentTarget: HTMLInputElement },
143+
) {
144+
if (ev.key.length > 1 || ev.ctrlKey || ev.metaKey) return
145+
if (ev.key === 'Tab') return
146+
if (
147+
!charIsNumber(ev.key) ||
148+
(ev.key === '.' && ev.currentTarget.value.includes('.'))
149+
) {
150+
ev.preventDefault()
151+
}
152+
}
153+
154+
function charIsNumber(char?: string) {
155+
return !!(char || '').match(/\d|\./)
156+
}

components/src/widget/views/confirmation/confirmation.css

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,6 @@
6969
flex: 1 1 auto;
7070
}
7171

72-
.preset-buttons {
73-
display: flex;
74-
justify-content: center;
75-
gap: 12px;
76-
margin: var(--Spacings-lg, 24px) 0 var(--Spacings-lg, 24px) 0;
77-
}
78-
7972
.enter-amount-description {
8073
color: var(--Text-paragraph-standard, #363636);
8174
font-family: var(--Font-Family-Inter, Inter);
@@ -85,40 +78,6 @@
8578
line-height: var(--Font-Line-Height-sm, 20px);
8679
}
8780

88-
.preset-btn {
89-
display: flex;
90-
width: calc(var(--Font-Size-text-base) + 2.75rem);
91-
height: calc(var(--Font-Size-text-base) + 2.75rem);
92-
cursor: pointer;
93-
padding: 16px 12px;
94-
justify-content: center;
95-
align-items: center;
96-
gap: 10px;
97-
border-radius: 100px;
98-
border: 1px solid var(--text-color, #c9c9c9);
99-
100-
background: var(--background-color, #f2fbf9);
101-
color: var(--Text-paragraph-standard, #7b7b7b);
102-
font-family: var(--Font-Family-Inter, Inter);
103-
font-size: var(--Font-Size-text-base, 16px);
104-
font-style: normal;
105-
font-weight: var(--Font-Weight-Bold, 700);
106-
line-height: var(--Font-Line-Height-md, 24px);
107-
}
108-
109-
.preset-btn:hover {
110-
background: #0000002f;
111-
}
112-
113-
.preset-btn.selected {
114-
background: color-mix(
115-
in srgb,
116-
var(--primary-color, #f2fbf9) 45%,
117-
transparent
118-
);
119-
border: 1px solid var(--primary-color, #5b5380);
120-
}
121-
12281
.widget-body p,
12382
.widget-body label:not(.form-label) {
12483
color: var(--Text-paragraph-standard, #363636);
@@ -136,19 +95,6 @@
13695
height: 100%;
13796
}
13897

139-
.currency-symbol {
140-
position: absolute;
141-
left: var(--Spacings-md, 12px);
142-
top: 50%;
143-
transform: translateY(-50%);
144-
color: var(--Text-paragraph-standard, #363636);
145-
font-family: var(--Font-Family-Inter, Inter);
146-
font-size: var(--Font-Size-text-base, 16px);
147-
font-weight: var(--Font-Weight-Regular, 400);
148-
pointer-events: none;
149-
z-index: 1;
150-
}
151-
15298
.payment-details {
15399
display: flex;
154100
padding: var(--Spacings-md, 12px);
@@ -352,16 +298,6 @@
352298
line-height: var(--Font-Line-Height-xs, 16px);
353299
}
354300

355-
.amount-input-wrapper {
356-
position: relative;
357-
width: 100%;
358-
}
359-
360-
.form-input.with-currency {
361-
padding-left: calc(var(--Font-Size-text-base) + 2rem);
362-
border: 2px solid var(--Colors-silver-300, #c9c9c9);
363-
}
364-
365301
.form-input,
366302
.payment-note-input {
367303
width: 100%;

0 commit comments

Comments
 (0)