@incodetech/web 2.0.0-alpha.1 → 2.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base.css +1071 -0
- package/dist/browser-ponyfill-B6W6hHVY.js +344 -0
- package/dist/email/email.es.js +145 -0
- package/dist/email/styles.css +123 -0
- package/dist/flow/flow.es.js +555 -0
- package/dist/flow/styles.css +1119 -0
- package/dist/incodeModule-Dv8Qllrv.js +254 -0
- package/dist/index.es.js +6 -0
- package/dist/instance-B-q0ZREN.js +2140 -0
- package/dist/otpInput-BtoZe0Wz.js +151 -0
- package/dist/page-Dh_Zw2ik.js +234 -0
- package/dist/phone/phone.es.js +3441 -0
- package/dist/phone/styles.css +305 -0
- package/dist/selfie/selfie.es.js +893 -0
- package/dist/selfie/styles.css +590 -0
- package/dist/selfieTutorial-C-u5GufD.js +29 -0
- package/dist/setup-wNL83jmW.js +20 -0
- package/dist/themes/dark.css +652 -0
- package/dist/themes/light.css +543 -0
- package/dist/title-BfO5Dlzk.js +25 -0
- package/dist/types/base.d.ts +1 -0
- package/dist/types/dark.d.ts +1 -0
- package/dist/types/email.d.ts +57 -0
- package/dist/types/flow.d.ts +69 -0
- package/dist/types/index.d.ts +38 -0
- package/dist/types/light.d.ts +1 -0
- package/dist/types/phone.d.ts +58 -0
- package/dist/types/selfie.d.ts +31 -0
- package/dist/types/styles.d.ts +1 -0
- package/dist/types/themes/dark.d.ts +1 -0
- package/dist/types/themes/light.d.ts +1 -0
- package/dist/uiConfig-CQ1W9cUD.js +23 -0
- package/dist/vendor-preact-CK0WeTOR.js +584 -0
- package/package.json +32 -26
- package/dev/README.md +0 -163
- package/dev/getToken.ts +0 -36
- package/dev/headless.html +0 -875
- package/dev/index.html +0 -366
- package/dev/main-headless.tsx +0 -1332
- package/dev/main-orchestrated-flow.tsx +0 -1158
- package/dev/main-preact.tsx +0 -323
- package/dev/main-simplified.tsx +0 -123
- package/dev/main-web-component.tsx +0 -256
- package/dev/main.tsx +0 -332
- package/dev/manual.html +0 -27
- package/dev/orchestrated-flow.html +0 -64
- package/dev/simplified.html +0 -64
- package/dev/tiktok-logo.svg +0 -7
- package/src/defineCustomElement.tsx +0 -30
- package/src/email/email.test.tsx +0 -368
- package/src/email/email.tsx +0 -255
- package/src/email/emailInput.test.tsx +0 -264
- package/src/email/emailInput.tsx +0 -85
- package/src/email/styles.css +0 -59
- package/src/flow/flow.test.tsx +0 -796
- package/src/flow/flow.tsx +0 -292
- package/src/flow/flowCompleted.css +0 -30
- package/src/flow/flowCompleted.test.tsx +0 -331
- package/src/flow/flowCompleted.tsx +0 -121
- package/src/flow/flowInit.test.ts +0 -264
- package/src/flow/flowInit.ts +0 -94
- package/src/flow/flowStart.css +0 -58
- package/src/flow/flowStart.test.tsx +0 -49
- package/src/flow/flowStart.tsx +0 -41
- package/src/flow/incode-logo.svg +0 -8
- package/src/flow/index.ts +0 -7
- package/src/flow/preloadFlow.test.ts +0 -421
- package/src/flow/preloadFlow.ts +0 -171
- package/src/flow/styles.css +0 -9
- package/src/flow/unsupportedModule.css +0 -21
- package/src/flow/unsupportedModule.tsx +0 -39
- package/src/flow/useFlowInitialization.test.tsx +0 -292
- package/src/flow/useFlowInitialization.ts +0 -128
- package/src/flow/useModuleLoader.test.tsx +0 -212
- package/src/flow/useModuleLoader.ts +0 -92
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useManager.test.ts +0 -91
- package/src/hooks/useManager.ts +0 -40
- package/src/i18n/index.ts +0 -3
- package/src/i18n/instance.ts +0 -16
- package/src/i18n/setup.ts +0 -184
- package/src/i18n/useTranslation.ts +0 -42
- package/src/index.ts +0 -27
- package/src/permissions/assets/android-dots-icon.svg +0 -7
- package/src/permissions/assets/android-settings-icon.svg +0 -16
- package/src/permissions/assets/android-toggle-icon.svg +0 -20
- package/src/permissions/assets/bank-card-icon.svg +0 -14
- package/src/permissions/assets/camera-icon.svg +0 -12
- package/src/permissions/assets/camera-ios.svg +0 -53
- package/src/permissions/assets/check-icon.svg +0 -8
- package/src/permissions/assets/chrome-icon.svg +0 -43
- package/src/permissions/assets/password-icon.svg +0 -11
- package/src/permissions/assets/permissions-img.svg +0 -51
- package/src/permissions/assets/safari-icon.svg +0 -37
- package/src/permissions/assets/settings-icon.svg +0 -33
- package/src/permissions/assets/toggle-icon.svg +0 -19
- package/src/permissions/assets/warning-icon.svg +0 -6
- package/src/permissions/boldWithArrow.css +0 -9
- package/src/permissions/boldWithArrow.tsx +0 -41
- package/src/permissions/denied.css +0 -37
- package/src/permissions/denied.tsx +0 -29
- package/src/permissions/deniedAndroid.tsx +0 -56
- package/src/permissions/deniedDesktop.css +0 -9
- package/src/permissions/deniedDesktop.tsx +0 -64
- package/src/permissions/deniedIOS.tsx +0 -73
- package/src/permissions/deniedInstructions.tsx +0 -19
- package/src/permissions/iconWrapper.css +0 -9
- package/src/permissions/iconWrapper.tsx +0 -15
- package/src/permissions/learnMore.css +0 -37
- package/src/permissions/learnMore.tsx +0 -85
- package/src/permissions/numberedStep.css +0 -13
- package/src/permissions/numberedStep.tsx +0 -14
- package/src/permissions/permissions.css +0 -13
- package/src/permissions/permissions.tsx +0 -68
- package/src/phone/phone.tsx +0 -246
- package/src/phone/phoneInput.test.tsx +0 -275
- package/src/phone/phoneInput.tsx +0 -249
- package/src/phone/styles.css +0 -158
- package/src/selfie/cameraButton.css +0 -13
- package/src/selfie/cameraButton.tsx +0 -35
- package/src/selfie/capture.css +0 -57
- package/src/selfie/capture.tsx +0 -232
- package/src/selfie/errorModal.tsx +0 -218
- package/src/selfie/errorModalContent.css +0 -33
- package/src/selfie/errorModalContent.tsx +0 -44
- package/src/selfie/faceOutline.css +0 -5
- package/src/selfie/faceOutline.tsx +0 -22
- package/src/selfie/loadingBorder.css +0 -12
- package/src/selfie/loadingBorder.tsx +0 -77
- package/src/selfie/manualCaptureButton.css +0 -13
- package/src/selfie/manualCaptureButton.tsx +0 -35
- package/src/selfie/noMoreAttemptsModal.tsx +0 -44
- package/src/selfie/notification.css +0 -9
- package/src/selfie/notification.tsx +0 -36
- package/src/selfie/retryErrorModal.tsx +0 -56
- package/src/selfie/selfie.test.tsx +0 -458
- package/src/selfie/selfie.tsx +0 -83
- package/src/selfie/selfieTutorial.json +0 -2626
- package/src/selfie/styles.css +0 -1
- package/src/selfie/tutorial.test.tsx +0 -200
- package/src/selfie/tutorial.tsx +0 -43
- package/src/setup.ts +0 -33
- package/src/shared/baseTutorial/baseTutorial.css +0 -21
- package/src/shared/baseTutorial/baseTutorial.test.tsx +0 -184
- package/src/shared/baseTutorial/baseTutorial.tsx +0 -55
- package/src/shared/baseTutorial/replaceBaseTutorial.test.ts +0 -267
- package/src/shared/baseTutorial/replaceBaseTutorial.ts +0 -68
- package/src/shared/button/button.css +0 -55
- package/src/shared/button/button.test.tsx +0 -101
- package/src/shared/button/button.tsx +0 -47
- package/src/shared/componentRoot/incodeComponent.tsx +0 -12
- package/src/shared/countries/countries.test.ts +0 -75
- package/src/shared/countries/countries.ts +0 -139
- package/src/shared/countries/index.ts +0 -6
- package/src/shared/icons/chevronDown.tsx +0 -22
- package/src/shared/icons/index.ts +0 -2
- package/src/shared/icons/successIcon.css +0 -5
- package/src/shared/icons/successIcon.test.tsx +0 -40
- package/src/shared/icons/successIcon.tsx +0 -26
- package/src/shared/loader/loadingIcon.css +0 -28
- package/src/shared/loader/loadingIcon.tsx +0 -67
- package/src/shared/lottie/lottie.tsx +0 -108
- package/src/shared/otpInput/otpInput.css +0 -85
- package/src/shared/otpInput/otpInput.test.tsx +0 -356
- package/src/shared/otpInput/otpInput.tsx +0 -241
- package/src/shared/page/incode-logo.svg +0 -3
- package/src/shared/page/page.css +0 -47
- package/src/shared/page/page.test.tsx +0 -97
- package/src/shared/page/page.tsx +0 -91
- package/src/shared/page/pageUiConfig.test.ts +0 -112
- package/src/shared/page/pageUiConfig.ts +0 -64
- package/src/shared/page/verifiedByIncode.css +0 -5
- package/src/shared/page/verifiedByIncode.tsx +0 -75
- package/src/shared/spacer/spacer.css +0 -149
- package/src/shared/spacer/spacer.test.tsx +0 -143
- package/src/shared/spacer/spacer.tsx +0 -88
- package/src/shared/spinner/index.ts +0 -2
- package/src/shared/spinner/spinner.css +0 -28
- package/src/shared/spinner/spinner.test.tsx +0 -82
- package/src/shared/spinner/spinner.tsx +0 -65
- package/src/shared/title/title.css +0 -7
- package/src/shared/title/title.tsx +0 -12
- package/src/shared/uiConfig/uiConfig.ts +0 -36
- package/src/shared/webComponent/incodeModule.ts +0 -29
- package/src/shared/webComponent/registerIncodeElement.ts +0 -15
- package/src/styles/__mocks__/fetchTheme.ts +0 -19
- package/src/styles/applyTheme.ts +0 -37
- package/src/styles/cn.test.tsx +0 -57
- package/src/styles/cn.tsx +0 -21
- package/src/styles/core.css +0 -12
- package/src/styles/fetchTheme.test.ts +0 -390
- package/src/styles/fetchTheme.ts +0 -88
- package/src/styles/generatePalette.ts +0 -111
- package/src/styles/reset.css +0 -65
- package/src/styles/resolveCssVariableToHex.ts +0 -28
- package/src/styles/tailwind.css +0 -291
- package/src/styles/themeTypes.ts +0 -18
- package/src/styles/tokens/colors.css +0 -190
- package/src/styles/tokens/components.css +0 -174
- package/src/styles/tokens/index.css +0 -4
- package/src/styles/tokens/primitives.css +0 -129
- package/src/styles/tokens/semantic.css +0 -51
- package/src/svg.d.ts +0 -4
- package/src/types/assets.d.ts +0 -1
- package/src/types/custom-elements.d.ts +0 -104
- package/tsconfig.json +0 -22
- package/vite.config.ts +0 -260
- package/vitest.config.ts +0 -40
- package/vitest.setup.ts +0 -16
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/preact';
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { PhoneInput } from './phoneInput';
|
|
4
|
-
|
|
5
|
-
describe('PhoneInput', () => {
|
|
6
|
-
const defaultProps = {
|
|
7
|
-
countryCode: 'US',
|
|
8
|
-
phonePrefix: '+1',
|
|
9
|
-
onPhoneChange: vi.fn(),
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
vi.clearAllMocks();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('Rendering', () => {
|
|
17
|
-
it('renders phone input field', () => {
|
|
18
|
-
render(<PhoneInput {...defaultProps} />);
|
|
19
|
-
|
|
20
|
-
const input = screen.getByTestId('phone-input');
|
|
21
|
-
expect(input).toBeTruthy();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('renders country selector button', () => {
|
|
25
|
-
render(<PhoneInput {...defaultProps} />);
|
|
26
|
-
|
|
27
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
28
|
-
expect(selector).toBeTruthy();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('renders with initial country code', () => {
|
|
32
|
-
render(<PhoneInput {...defaultProps} countryCode="MX" />);
|
|
33
|
-
|
|
34
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
35
|
-
expect(selector.textContent).toContain('+52');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('renders US flag emoji for US country', () => {
|
|
39
|
-
render(<PhoneInput {...defaultProps} countryCode="US" />);
|
|
40
|
-
|
|
41
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
42
|
-
expect(selector.textContent).toContain('🇺🇸');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('Phone input', () => {
|
|
47
|
-
it('calls onPhoneChange when input changes', () => {
|
|
48
|
-
const onPhoneChange = vi.fn();
|
|
49
|
-
render(<PhoneInput {...defaultProps} onPhoneChange={onPhoneChange} />);
|
|
50
|
-
|
|
51
|
-
const input = screen.getByTestId('phone-input');
|
|
52
|
-
fireEvent.input(input, { target: { value: '4155551234' } });
|
|
53
|
-
|
|
54
|
-
expect(onPhoneChange).toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('formats phone number as user types', () => {
|
|
58
|
-
render(<PhoneInput {...defaultProps} />);
|
|
59
|
-
|
|
60
|
-
const input = screen.getByTestId('phone-input') as HTMLInputElement;
|
|
61
|
-
fireEvent.input(input, { target: { value: '4155551234' } });
|
|
62
|
-
|
|
63
|
-
// Should be formatted with AsYouType
|
|
64
|
-
expect(input.value).toMatch(/[\d\s()-]+/);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('validates phone number and reports validity', () => {
|
|
68
|
-
const onPhoneChange = vi.fn();
|
|
69
|
-
render(<PhoneInput {...defaultProps} onPhoneChange={onPhoneChange} />);
|
|
70
|
-
|
|
71
|
-
const input = screen.getByTestId('phone-input');
|
|
72
|
-
|
|
73
|
-
// Invalid phone (too short)
|
|
74
|
-
fireEvent.input(input, { target: { value: '123' } });
|
|
75
|
-
expect(onPhoneChange).toHaveBeenLastCalledWith(expect.any(String), false);
|
|
76
|
-
|
|
77
|
-
// Valid US phone
|
|
78
|
-
fireEvent.input(input, { target: { value: '4155551234' } });
|
|
79
|
-
expect(onPhoneChange).toHaveBeenLastCalledWith(expect.any(String), true);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('Country selector', () => {
|
|
84
|
-
it('opens dropdown on click', () => {
|
|
85
|
-
render(<PhoneInput {...defaultProps} />);
|
|
86
|
-
|
|
87
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
88
|
-
fireEvent.click(selector);
|
|
89
|
-
|
|
90
|
-
// Dropdown should be visible
|
|
91
|
-
const dropdown = document.querySelector('.IncodePhoneCountryDropdown');
|
|
92
|
-
expect(dropdown).toBeTruthy();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('closes dropdown when clicking outside', () => {
|
|
96
|
-
render(<PhoneInput {...defaultProps} />);
|
|
97
|
-
|
|
98
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
99
|
-
fireEvent.click(selector);
|
|
100
|
-
|
|
101
|
-
// Click outside
|
|
102
|
-
fireEvent.mouseDown(document.body);
|
|
103
|
-
|
|
104
|
-
const dropdown = document.querySelector('.IncodePhoneCountryDropdown');
|
|
105
|
-
expect(dropdown).toBeFalsy();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('shows list of countries in dropdown', () => {
|
|
109
|
-
render(<PhoneInput {...defaultProps} />);
|
|
110
|
-
|
|
111
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
112
|
-
fireEvent.click(selector);
|
|
113
|
-
|
|
114
|
-
const options = document.querySelectorAll('.IncodePhoneCountryOption');
|
|
115
|
-
expect(options.length).toBeGreaterThan(0);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('changes country when option is selected', () => {
|
|
119
|
-
const onPhoneChange = vi.fn();
|
|
120
|
-
render(<PhoneInput {...defaultProps} onPhoneChange={onPhoneChange} />);
|
|
121
|
-
|
|
122
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
123
|
-
fireEvent.click(selector);
|
|
124
|
-
|
|
125
|
-
// Find and click Mexico option
|
|
126
|
-
const options = document.querySelectorAll('.IncodePhoneCountryOption');
|
|
127
|
-
const mxOption = Array.from(options).find((opt) =>
|
|
128
|
-
opt.textContent?.includes('Mexico'),
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
if (mxOption) {
|
|
132
|
-
fireEvent.click(mxOption);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Dropdown should close and country should change
|
|
136
|
-
expect(selector.textContent).toContain('+52');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('does not open dropdown when disabled', () => {
|
|
140
|
-
render(<PhoneInput {...defaultProps} disabled />);
|
|
141
|
-
|
|
142
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
143
|
-
fireEvent.click(selector);
|
|
144
|
-
|
|
145
|
-
const dropdown = document.querySelector('.IncodePhoneCountryDropdown');
|
|
146
|
-
expect(dropdown).toBeFalsy();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('Prefilled phone', () => {
|
|
151
|
-
it('populates input with prefilled phone', () => {
|
|
152
|
-
render(<PhoneInput {...defaultProps} prefilledPhone="+14155551234" />);
|
|
153
|
-
|
|
154
|
-
const input = screen.getByTestId('phone-input') as HTMLInputElement;
|
|
155
|
-
expect(input.value).toBeTruthy();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('calls onPhoneChange with prefilled phone', () => {
|
|
159
|
-
const onPhoneChange = vi.fn();
|
|
160
|
-
render(
|
|
161
|
-
<PhoneInput
|
|
162
|
-
{...defaultProps}
|
|
163
|
-
prefilledPhone="+14155551234"
|
|
164
|
-
onPhoneChange={onPhoneChange}
|
|
165
|
-
/>,
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// Should have called onPhoneChange with the prefilled value
|
|
169
|
-
expect(onPhoneChange).toHaveBeenCalledWith('+14155551234', true);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('Error state', () => {
|
|
174
|
-
it('displays error message when phoneError is set', () => {
|
|
175
|
-
render(
|
|
176
|
-
<PhoneInput {...defaultProps} phoneError="Invalid phone number" />,
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
expect(screen.getByTestId('phone-error')).toBeTruthy();
|
|
180
|
-
expect(screen.getByText('Invalid phone number')).toBeTruthy();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('shows error styling on input wrapper', () => {
|
|
184
|
-
const { container } = render(
|
|
185
|
-
<PhoneInput {...defaultProps} phoneError="Error" />,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
const wrapper = container.querySelector('.IncodePhoneInputWrapperError');
|
|
189
|
-
expect(wrapper).toBeTruthy();
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe('Opt-in checkbox', () => {
|
|
194
|
-
it('does not show opt-in when optinEnabled is false', () => {
|
|
195
|
-
render(<PhoneInput {...defaultProps} optinEnabled={false} />);
|
|
196
|
-
|
|
197
|
-
expect(screen.queryByTestId('phone-optin')).toBeNull();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('shows opt-in checkbox when optinEnabled is true', () => {
|
|
201
|
-
render(<PhoneInput {...defaultProps} optinEnabled />);
|
|
202
|
-
|
|
203
|
-
expect(screen.getByTestId('phone-optin')).toBeTruthy();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('calls onOptInChange when checkbox is toggled', () => {
|
|
207
|
-
const onOptInChange = vi.fn();
|
|
208
|
-
render(
|
|
209
|
-
<PhoneInput
|
|
210
|
-
{...defaultProps}
|
|
211
|
-
optinEnabled
|
|
212
|
-
onOptInChange={onOptInChange}
|
|
213
|
-
/>,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const checkbox = screen.getByTestId('phone-optin');
|
|
217
|
-
fireEvent.change(checkbox, { target: { checked: true } });
|
|
218
|
-
|
|
219
|
-
expect(onOptInChange).toHaveBeenCalledWith(true);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe('Disabled state', () => {
|
|
224
|
-
it('disables input when disabled prop is true', () => {
|
|
225
|
-
render(<PhoneInput {...defaultProps} disabled />);
|
|
226
|
-
|
|
227
|
-
const input = screen.getByTestId('phone-input') as HTMLInputElement;
|
|
228
|
-
expect(input.disabled).toBe(true);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('disables country selector when disabled', () => {
|
|
232
|
-
render(<PhoneInput {...defaultProps} disabled />);
|
|
233
|
-
|
|
234
|
-
const button = screen.getByTestId('phone-country-selector');
|
|
235
|
-
expect(button.hasAttribute('disabled')).toBe(true);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('shows disabled styling', () => {
|
|
239
|
-
const { container } = render(<PhoneInput {...defaultProps} disabled />);
|
|
240
|
-
|
|
241
|
-
const wrapper = container.querySelector(
|
|
242
|
-
'.IncodePhoneInputWrapperDisabled',
|
|
243
|
-
);
|
|
244
|
-
expect(wrapper).toBeTruthy();
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('Accessibility', () => {
|
|
249
|
-
it('country selector button is accessible', () => {
|
|
250
|
-
render(<PhoneInput {...defaultProps} />);
|
|
251
|
-
|
|
252
|
-
// Button should be accessible - it contains flag and country code
|
|
253
|
-
const button = screen.getByTestId('phone-country-selector');
|
|
254
|
-
expect(button.tagName.toLowerCase()).toBe('button');
|
|
255
|
-
expect(button.textContent).toContain('+1'); // Country code is visible
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('has aria-expanded attribute on country selector', () => {
|
|
259
|
-
render(<PhoneInput {...defaultProps} />);
|
|
260
|
-
|
|
261
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
262
|
-
expect(selector.getAttribute('aria-expanded')).toBe('false');
|
|
263
|
-
|
|
264
|
-
fireEvent.click(selector);
|
|
265
|
-
expect(selector.getAttribute('aria-expanded')).toBe('true');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('has aria-haspopup attribute on country selector', () => {
|
|
269
|
-
render(<PhoneInput {...defaultProps} />);
|
|
270
|
-
|
|
271
|
-
const selector = screen.getByTestId('phone-country-selector');
|
|
272
|
-
expect(selector.getAttribute('aria-haspopup')).toBe('listbox');
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
});
|
package/src/phone/phoneInput.tsx
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AsYouType,
|
|
3
|
-
type CountryCode,
|
|
4
|
-
parsePhoneNumberFromString,
|
|
5
|
-
} from 'libphonenumber-js';
|
|
6
|
-
import type { FC } from 'preact/compat';
|
|
7
|
-
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
8
|
-
import {
|
|
9
|
-
type CountryData,
|
|
10
|
-
countries,
|
|
11
|
-
getCountryByCode,
|
|
12
|
-
} from '../shared/countries';
|
|
13
|
-
import { ChevronDown } from '../shared/icons';
|
|
14
|
-
import { Spacer } from '../shared/spacer/spacer';
|
|
15
|
-
import './styles.css';
|
|
16
|
-
|
|
17
|
-
export type PhoneInputProps = {
|
|
18
|
-
countryCode: string;
|
|
19
|
-
phonePrefix: string;
|
|
20
|
-
prefilledPhone?: string;
|
|
21
|
-
phoneError?: string;
|
|
22
|
-
optinEnabled?: boolean;
|
|
23
|
-
disabled?: boolean;
|
|
24
|
-
onPhoneChange: (phone: string, isValid: boolean) => void;
|
|
25
|
-
onOptInChange?: (granted: boolean) => void;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const PhoneInput: FC<PhoneInputProps> = ({
|
|
29
|
-
countryCode,
|
|
30
|
-
prefilledPhone,
|
|
31
|
-
phoneError,
|
|
32
|
-
optinEnabled = false,
|
|
33
|
-
disabled = false,
|
|
34
|
-
onPhoneChange,
|
|
35
|
-
onOptInChange,
|
|
36
|
-
}) => {
|
|
37
|
-
const [selectedCountry, setSelectedCountry] = useState<CountryData | null>(
|
|
38
|
-
null,
|
|
39
|
-
);
|
|
40
|
-
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
41
|
-
const [phone, setPhone] = useState('');
|
|
42
|
-
const [optInGranted, setOptInGranted] = useState(false);
|
|
43
|
-
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
44
|
-
|
|
45
|
-
// Initialize selected country from countryCode prop
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
const country = getCountryByCode(countryCode);
|
|
48
|
-
if (country) {
|
|
49
|
-
setSelectedCountry(country);
|
|
50
|
-
} else {
|
|
51
|
-
// Default to US if not found
|
|
52
|
-
setSelectedCountry(
|
|
53
|
-
countries.find((c) => c.code === 'US') || countries[0],
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
}, [countryCode]);
|
|
57
|
-
|
|
58
|
-
// Handle prefilled phone
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (prefilledPhone && selectedCountry) {
|
|
61
|
-
// Parse the prefilled phone to extract national number
|
|
62
|
-
const phoneToFormat = prefilledPhone.startsWith('+')
|
|
63
|
-
? prefilledPhone
|
|
64
|
-
: `+${prefilledPhone}`;
|
|
65
|
-
const parsed = parsePhoneNumberFromString(phoneToFormat);
|
|
66
|
-
|
|
67
|
-
if (parsed?.isValid()) {
|
|
68
|
-
// Update country if different
|
|
69
|
-
if (parsed.country && parsed.country !== selectedCountry.code) {
|
|
70
|
-
const newCountry = getCountryByCode(parsed.country);
|
|
71
|
-
if (newCountry) {
|
|
72
|
-
setSelectedCountry(newCountry);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Format and set the national number
|
|
76
|
-
const formatter = new AsYouType(parsed.country);
|
|
77
|
-
const formatted = formatter.input(parsed.nationalNumber);
|
|
78
|
-
setPhone(formatted);
|
|
79
|
-
onPhoneChange(phoneToFormat, true);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}, [prefilledPhone, selectedCountry]);
|
|
83
|
-
|
|
84
|
-
// Close dropdown when clicking outside
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
87
|
-
if (
|
|
88
|
-
dropdownRef.current &&
|
|
89
|
-
!dropdownRef.current.contains(e.target as Node)
|
|
90
|
-
) {
|
|
91
|
-
setIsDropdownOpen(false);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
if (isDropdownOpen) {
|
|
96
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
97
|
-
}
|
|
98
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
99
|
-
}, [isDropdownOpen]);
|
|
100
|
-
|
|
101
|
-
const validateAndNotify = (nationalNumber: string, country: CountryData) => {
|
|
102
|
-
const fullNumber = `${country.callingCode}${nationalNumber.replace(/\D/g, '')}`;
|
|
103
|
-
try {
|
|
104
|
-
const parsed = parsePhoneNumberFromString(
|
|
105
|
-
fullNumber,
|
|
106
|
-
country.code as CountryCode,
|
|
107
|
-
);
|
|
108
|
-
const valid = parsed?.isValid() ?? false;
|
|
109
|
-
onPhoneChange(fullNumber, valid);
|
|
110
|
-
} catch {
|
|
111
|
-
onPhoneChange(fullNumber, false);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const handlePhoneChange = (e: Event) => {
|
|
116
|
-
const target = e.target as HTMLInputElement;
|
|
117
|
-
const rawValue = target.value;
|
|
118
|
-
|
|
119
|
-
if (selectedCountry) {
|
|
120
|
-
// Format as user types
|
|
121
|
-
const formatter = new AsYouType(selectedCountry.code);
|
|
122
|
-
const formatted = formatter.input(rawValue);
|
|
123
|
-
setPhone(formatted);
|
|
124
|
-
validateAndNotify(rawValue, selectedCountry);
|
|
125
|
-
} else {
|
|
126
|
-
setPhone(rawValue);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const handleCountrySelect = (country: CountryData) => {
|
|
131
|
-
setSelectedCountry(country);
|
|
132
|
-
setIsDropdownOpen(false);
|
|
133
|
-
|
|
134
|
-
// Revalidate with new country
|
|
135
|
-
if (phone) {
|
|
136
|
-
validateAndNotify(phone, country);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const handleOptInChange = (e: Event) => {
|
|
141
|
-
const target = e.target as HTMLInputElement;
|
|
142
|
-
const granted = target.checked;
|
|
143
|
-
setOptInGranted(granted);
|
|
144
|
-
onOptInChange?.(granted);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const toggleDropdown = () => {
|
|
148
|
-
if (!disabled) {
|
|
149
|
-
setIsDropdownOpen(!isDropdownOpen);
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<div class="IncodePhoneInput">
|
|
155
|
-
<div class="IncodePhoneInputContainer">
|
|
156
|
-
<div class="IncodePhoneInputRow">
|
|
157
|
-
{/* Country Selector */}
|
|
158
|
-
<div
|
|
159
|
-
class={`IncodePhoneCountrySelector ${isDropdownOpen ? 'IncodePhoneCountrySelectorOpen' : ''} ${disabled ? 'IncodePhoneCountrySelectorDisabled' : ''}`}
|
|
160
|
-
ref={dropdownRef}
|
|
161
|
-
>
|
|
162
|
-
<button
|
|
163
|
-
type="button"
|
|
164
|
-
class="IncodePhoneCountryButton"
|
|
165
|
-
onClick={toggleDropdown}
|
|
166
|
-
disabled={disabled}
|
|
167
|
-
aria-expanded={isDropdownOpen}
|
|
168
|
-
aria-haspopup="listbox"
|
|
169
|
-
data-testid="phone-country-selector"
|
|
170
|
-
>
|
|
171
|
-
<span class="IncodePhoneCountryFlag" aria-hidden="true">
|
|
172
|
-
{selectedCountry?.emoji}
|
|
173
|
-
</span>
|
|
174
|
-
<span class="IncodePhoneCountryCode">
|
|
175
|
-
{selectedCountry?.callingCode}
|
|
176
|
-
</span>
|
|
177
|
-
<ChevronDown
|
|
178
|
-
class={`IncodePhoneCountryArrow ${isDropdownOpen ? 'IncodePhoneCountryArrowUp' : ''}`}
|
|
179
|
-
/>
|
|
180
|
-
</button>
|
|
181
|
-
|
|
182
|
-
{/* Country Dropdown */}
|
|
183
|
-
{isDropdownOpen && (
|
|
184
|
-
<div class="IncodePhoneCountryDropdown">
|
|
185
|
-
{countries.map((country) => (
|
|
186
|
-
<button
|
|
187
|
-
key={country.code}
|
|
188
|
-
type="button"
|
|
189
|
-
class={`IncodePhoneCountryOption ${selectedCountry?.code === country.code ? 'IncodePhoneCountryOptionSelected' : ''}`}
|
|
190
|
-
onClick={() => handleCountrySelect(country)}
|
|
191
|
-
>
|
|
192
|
-
<span class="IncodePhoneCountryFlag" aria-hidden="true">
|
|
193
|
-
{country.emoji}
|
|
194
|
-
</span>
|
|
195
|
-
<span class="IncodePhoneCountryOptionCode">
|
|
196
|
-
{country.callingCode}
|
|
197
|
-
</span>
|
|
198
|
-
<span class="IncodePhoneCountryOptionName">
|
|
199
|
-
{country.name}
|
|
200
|
-
</span>
|
|
201
|
-
</button>
|
|
202
|
-
))}
|
|
203
|
-
</div>
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
{/* Phone Input */}
|
|
208
|
-
<div
|
|
209
|
-
class={`IncodePhoneInputWrapper ${phoneError ? 'IncodePhoneInputWrapperError' : ''} ${disabled ? 'IncodePhoneInputWrapperDisabled' : ''}`}
|
|
210
|
-
>
|
|
211
|
-
<input
|
|
212
|
-
type="tel"
|
|
213
|
-
value={phone}
|
|
214
|
-
onInput={handlePhoneChange}
|
|
215
|
-
placeholder="(123) 456-7890"
|
|
216
|
-
disabled={disabled}
|
|
217
|
-
class="IncodePhoneInputField"
|
|
218
|
-
data-testid="phone-input"
|
|
219
|
-
/>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
{phoneError && (
|
|
224
|
-
<>
|
|
225
|
-
<Spacer size={8} />
|
|
226
|
-
<p class="IncodePhoneInputError" data-testid="phone-error">
|
|
227
|
-
{phoneError}
|
|
228
|
-
</p>
|
|
229
|
-
</>
|
|
230
|
-
)}
|
|
231
|
-
|
|
232
|
-
{optinEnabled && (
|
|
233
|
-
<>
|
|
234
|
-
<Spacer size={16} />
|
|
235
|
-
<label class="IncodePhoneInputOptIn">
|
|
236
|
-
<input
|
|
237
|
-
type="checkbox"
|
|
238
|
-
checked={optInGranted}
|
|
239
|
-
onChange={handleOptInChange}
|
|
240
|
-
data-testid="phone-optin"
|
|
241
|
-
/>
|
|
242
|
-
<span>I agree to receive SMS notifications</span>
|
|
243
|
-
</label>
|
|
244
|
-
</>
|
|
245
|
-
)}
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
);
|
|
249
|
-
};
|
package/src/phone/styles.css
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
@reference "../styles/tailwind.css";
|
|
2
|
-
|
|
3
|
-
/* Phone Page Layout */
|
|
4
|
-
.IncodePhonePage .IncodePageContent {
|
|
5
|
-
@apply pt-32 pb-16;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.IncodePhoneInput {
|
|
9
|
-
@apply flex flex-col items-center w-full;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.IncodePhoneInputContainer {
|
|
13
|
-
@apply flex flex-col items-center w-full;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/* Row container for country selector and input */
|
|
17
|
-
.IncodePhoneInputRow {
|
|
18
|
-
@apply relative flex items-stretch gap-8 w-full max-w-[340px];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Phone Input Wrapper (separate box) */
|
|
22
|
-
.IncodePhoneInputWrapper {
|
|
23
|
-
@apply flex-1 flex items-center border border-input-border-default rounded-small bg-input-surface-default transition-colors duration-200;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.IncodePhoneInputWrapper:focus-within {
|
|
27
|
-
@apply border-input-border-active;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.IncodePhoneInputWrapper.IncodePhoneInputWrapperError {
|
|
31
|
-
@apply border-input-border-negative;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.IncodePhoneInputWrapper.IncodePhoneInputWrapperDisabled {
|
|
35
|
-
@apply border-input-border-disabled bg-input-surface-disabled;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/* Country Selector (separate box) */
|
|
39
|
-
.IncodePhoneCountrySelector {
|
|
40
|
-
@apply static shrink-0 w-[124px];
|
|
41
|
-
@apply border border-input-border-default rounded-small bg-input-surface-default;
|
|
42
|
-
@apply transition-colors duration-200;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.IncodePhoneCountrySelector.IncodePhoneCountrySelectorOpen {
|
|
46
|
-
@apply border-input-border-active;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.IncodePhoneCountrySelector.IncodePhoneCountrySelectorDisabled {
|
|
50
|
-
@apply border-input-border-disabled bg-input-surface-disabled;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.IncodePhoneCountryButton {
|
|
54
|
-
@apply flex items-center justify-center w-full h-full;
|
|
55
|
-
@apply p-8 pl-16;
|
|
56
|
-
@apply bg-transparent border-none cursor-pointer;
|
|
57
|
-
@apply text-18 font-medium text-text-body-primary;
|
|
58
|
-
@apply rounded-small;
|
|
59
|
-
font-family: inherit;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.IncodePhoneCountryButton:disabled {
|
|
63
|
-
@apply cursor-not-allowed text-input-text-field-disabled;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.IncodePhoneCountryButton:focus {
|
|
67
|
-
@apply outline-none;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.IncodePhoneCountryFlag {
|
|
71
|
-
@apply text-24 leading-none;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.IncodePhoneCountryCode {
|
|
75
|
-
@apply text-18 font-medium whitespace-nowrap text-text-body-primary ml-4;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.IncodePhoneCountryArrow {
|
|
79
|
-
@apply w-24 h-24 text-icon-neutral-500 transition-transform duration-200 ml-8;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.IncodePhoneCountryArrowUp {
|
|
83
|
-
@apply rotate-180;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.IncodePhoneCountryButton:disabled .IncodePhoneCountryArrow {
|
|
87
|
-
@apply text-icon-neutral-300;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/* Country Dropdown */
|
|
91
|
-
.IncodePhoneCountryDropdown {
|
|
92
|
-
@apply absolute top-full left-0 right-0 z-50 mt-8;
|
|
93
|
-
@apply max-h-[320px] overflow-y-auto;
|
|
94
|
-
@apply bg-surface-neutral-0 border border-input-border-default rounded-small;
|
|
95
|
-
@apply p-8;
|
|
96
|
-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.IncodePhoneCountryOption {
|
|
100
|
-
@apply w-full flex items-center gap-12 py-12 px-12 cursor-pointer rounded-small;
|
|
101
|
-
@apply text-16 font-medium text-text-body-primary;
|
|
102
|
-
@apply bg-transparent border-none text-left;
|
|
103
|
-
font-family: inherit;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.IncodePhoneCountryOption:hover {
|
|
107
|
-
@apply bg-surface-neutral-100;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.IncodePhoneCountryOption:focus {
|
|
111
|
-
@apply outline-none bg-surface-neutral-100;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.IncodePhoneCountryOptionSelected {
|
|
115
|
-
@apply bg-surface-brand-50;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.IncodePhoneCountryOptionSelected:hover {
|
|
119
|
-
@apply bg-surface-brand-50;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
.IncodePhoneCountryOptionCode {
|
|
123
|
-
@apply text-16 text-text-body-secondary min-w-[52px];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.IncodePhoneCountryOptionName {
|
|
127
|
-
@apply text-16 text-text-body-primary flex-1 truncate;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/* Phone Input Field */
|
|
131
|
-
.IncodePhoneInputField {
|
|
132
|
-
@apply w-full py-16 px-16 text-18 font-medium border-none outline-none bg-transparent text-input-text-field-default;
|
|
133
|
-
@apply rounded-small;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.IncodePhoneInputField::placeholder {
|
|
137
|
-
@apply text-input-text-field-placeholder font-medium;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.IncodePhoneInputField:disabled {
|
|
141
|
-
@apply cursor-not-allowed text-input-text-field-disabled;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.IncodePhoneInputError {
|
|
145
|
-
@apply text-input-text-helper-negative text-14 text-center m-0 font-medium;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.IncodePhoneInputOptIn {
|
|
149
|
-
@apply flex items-center gap-8 text-14 text-checkbox-text-default cursor-pointer;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.IncodePhoneInputOptIn input[type="checkbox"] {
|
|
153
|
-
@apply w-[18px] h-[18px] cursor-pointer accent-checkbox-surface-selected rounded-checkbox;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.IncodePhoneInputOptIn span {
|
|
157
|
-
@apply select-none font-medium;
|
|
158
|
-
}
|