From 86030ab97c023606c95bccc2e6a5408a545d57c7 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Tue, 15 Mar 2022 17:31:13 +0800 Subject: [PATCH] feat(ui): implement phonenumber input field (#372) * feat(ui): implement phonenumber input field implement phonenumber input field * fix(ui): phone input ui fix phone input ui fix * fix(ui): should not show error if not interacted should not show error if not interacted * fix(ui): fix styling fix styling * feat(ui): add typeMode for phone input add typeMode for phone input * chore(ui): update pnpm-lock update pnpm-lock --- packages/ui/jest.config.ts | 1 + packages/ui/package.json | 3 + packages/ui/src/assets/icons/arrow.svg | 5 + packages/ui/src/assets/icons/close-icon.svg | 3 + packages/ui/src/assets/icons/privacy-icon.svg | 20 +++ .../components/AppContent/index.module.scss | 2 +- .../ui/src/components/Icons/ClearIcon.tsx | 4 +- .../ui/src/components/Icons/DownArrowIcon.tsx | 13 ++ .../ui/src/components/Icons/PrivacyIcon.tsx | 24 +--- packages/ui/src/components/Icons/index.ts | 1 + .../components/Input/PasswordInput.test.tsx | 11 +- .../ui/src/components/Input/PasswordInput.tsx | 18 +-- .../src/components/Input/PhoneInput.test.tsx | 80 +++++++++++ .../ui/src/components/Input/PhoneInput.tsx | 111 +++++++++++++++ .../ui/src/components/Input/index.module.scss | 46 ++++--- .../ui/src/components/Input/index.test.tsx | 11 +- packages/ui/src/components/Input/index.tsx | 15 +- .../components/Input/phoneInput.module.scss | 26 ++++ .../PhoneInputProvider/index.test.tsx | 79 +++++++++++ .../containers/PhoneInputProvider/index.tsx | 38 ++++++ packages/ui/src/hooks/use-phone-number.ts | 128 ++++++++++++++++++ packages/ui/src/include.d/dom.d.ts | 3 +- packages/ui/src/scss/_underscore.scss | 6 + pnpm-lock.yaml | 83 +++++++++--- 24 files changed, 649 insertions(+), 82 deletions(-) create mode 100644 packages/ui/src/assets/icons/arrow.svg create mode 100644 packages/ui/src/assets/icons/close-icon.svg create mode 100644 packages/ui/src/assets/icons/privacy-icon.svg create mode 100644 packages/ui/src/components/Icons/DownArrowIcon.tsx create mode 100644 packages/ui/src/components/Input/PhoneInput.test.tsx create mode 100644 packages/ui/src/components/Input/PhoneInput.tsx create mode 100644 packages/ui/src/components/Input/phoneInput.module.scss create mode 100644 packages/ui/src/containers/PhoneInputProvider/index.test.tsx create mode 100644 packages/ui/src/containers/PhoneInputProvider/index.tsx create mode 100644 packages/ui/src/hooks/use-phone-number.ts diff --git a/packages/ui/jest.config.ts b/packages/ui/jest.config.ts index d278a4cb5..eca10606d 100644 --- a/packages/ui/jest.config.ts +++ b/packages/ui/jest.config.ts @@ -6,6 +6,7 @@ const config: Config.InitialOptions = { transform: { // Enable JS/JSX transformation '\\.(ts|js)x?$': 'ts-jest', + '\\.(svg)$': 'jest-transform-stub', }, transformIgnorePatterns: [ '[/\\\\]node_modules[/\\\\]((?!ky[/\\\\]).)+\\.(js|jsx|mjs|cjs|ts|tsx)$', diff --git a/packages/ui/package.json b/packages/ui/package.json index 38e5e4968..255de8325 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -21,9 +21,11 @@ "i18next": "^21.6.11", "i18next-browser-languagedetector": "^6.1.3", "ky": "^0.29.0", + "libphonenumber-js": "^1.9.49", "react": "^17.0.2", "react-dom": "^17.0.2", "react-i18next": "^11.15.4", + "react-phone-number-input": "^3.1.46", "react-router-dom": "^5.2.0" }, "devDependencies": { @@ -42,6 +44,7 @@ "eslint": "^8.10.0", "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", + "jest-transform-stub": "^2.0.0", "lint-staged": "^11.1.1", "parcel": "^2.3.2", "postcss": "^8.4.6", diff --git a/packages/ui/src/assets/icons/arrow.svg b/packages/ui/src/assets/icons/arrow.svg new file mode 100644 index 000000000..5b77a8a0b --- /dev/null +++ b/packages/ui/src/assets/icons/arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/icons/close-icon.svg b/packages/ui/src/assets/icons/close-icon.svg new file mode 100644 index 000000000..767c1b7e2 --- /dev/null +++ b/packages/ui/src/assets/icons/close-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/privacy-icon.svg b/packages/ui/src/assets/icons/privacy-icon.svg new file mode 100644 index 000000000..9d084fa60 --- /dev/null +++ b/packages/ui/src/assets/icons/privacy-icon.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/packages/ui/src/components/AppContent/index.module.scss b/packages/ui/src/components/AppContent/index.module.scss index 1280349a3..362cf0a54 100644 --- a/packages/ui/src/components/AppContent/index.module.scss +++ b/packages/ui/src/components/AppContent/index.module.scss @@ -52,7 +52,7 @@ --color-gradient: linear-gradient(69.73deg, #492ef3 5.52%, #cf69ff 94.22%); --color-background: #fdfdff; --color-control-background: #f4f4f4; - --color-control-focus: #a48dfa; + --color-control-focus: #{rgba(#a48dfa, 0.8)}; --color-neutral-100: #111; --color-neutral-90: #666; diff --git a/packages/ui/src/components/Icons/ClearIcon.tsx b/packages/ui/src/components/Icons/ClearIcon.tsx index c6ac718af..8985ef19b 100644 --- a/packages/ui/src/components/Icons/ClearIcon.tsx +++ b/packages/ui/src/components/Icons/ClearIcon.tsx @@ -1,8 +1,10 @@ import React, { SVGProps } from 'react'; +import CloseIcon from '@/assets/icons/close-icon.svg'; + const ClearIcon = (props: SVGProps) => ( - + ); diff --git a/packages/ui/src/components/Icons/DownArrowIcon.tsx b/packages/ui/src/components/Icons/DownArrowIcon.tsx new file mode 100644 index 000000000..2e45883c5 --- /dev/null +++ b/packages/ui/src/components/Icons/DownArrowIcon.tsx @@ -0,0 +1,13 @@ +import React, { SVGProps } from 'react'; + +import Arrow from '@/assets/icons/arrow.svg'; + +const DownArrowIcon = (props: SVGProps) => { + return ( + + + + ); +}; + +export default DownArrowIcon; diff --git a/packages/ui/src/components/Icons/PrivacyIcon.tsx b/packages/ui/src/components/Icons/PrivacyIcon.tsx index 656550a09..716ddd993 100644 --- a/packages/ui/src/components/Icons/PrivacyIcon.tsx +++ b/packages/ui/src/components/Icons/PrivacyIcon.tsx @@ -1,33 +1,15 @@ import React, { SVGProps } from 'react'; +import Icon from '@/assets/icons/privacy-icon.svg'; + type Props = { type?: 'show' | 'hide'; } & SVGProps; const PrivacyIcon = ({ type = 'show', ...rest }: Props) => { - if (type === 'hide') { - return ( - - - - - ); - } - return ( - + ); }; diff --git a/packages/ui/src/components/Icons/index.ts b/packages/ui/src/components/Icons/index.ts index 4c31efc1c..a58dde827 100644 --- a/packages/ui/src/components/Icons/index.ts +++ b/packages/ui/src/components/Icons/index.ts @@ -1,2 +1,3 @@ export { default as ClearIcon } from './ClearIcon'; export { default as PrivacyIcon } from './PrivacyIcon'; +export { default as DownArrowIcon } from './DownArrowIcon'; diff --git a/packages/ui/src/components/Input/PasswordInput.test.tsx b/packages/ui/src/components/Input/PasswordInput.test.tsx index 809837f25..b5aa9b09b 100644 --- a/packages/ui/src/components/Input/PasswordInput.test.tsx +++ b/packages/ui/src/components/Input/PasswordInput.test.tsx @@ -24,12 +24,19 @@ describe('Input Field UI Component', () => { const { container } = render(); const inputEle = container.querySelector('input'); + + if (!inputEle) { + return; + } + + fireEvent.focus(inputEle); + const visibilityButton = container.querySelector('svg'); expect(visibilityButton).not.toBeNull(); if (visibilityButton) { - fireEvent.click(visibilityButton); - expect(inputEle?.type).toEqual('text'); + fireEvent.mouseDown(visibilityButton); + expect(inputEle.type).toEqual('text'); } }); }); diff --git a/packages/ui/src/components/Input/PasswordInput.tsx b/packages/ui/src/components/Input/PasswordInput.tsx index e71ce813a..30daa6ac1 100644 --- a/packages/ui/src/components/Input/PasswordInput.tsx +++ b/packages/ui/src/components/Input/PasswordInput.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; +import React, { useState } from 'react'; import { PrivacyIcon } from '../Icons'; import * as styles from './index.module.scss'; @@ -25,20 +25,23 @@ const PasswordInput = ({ hasError = false, onChange, }: Props) => { - const inputReference = useRef(null); - - // Used to toggle the password visibility + // Toggle the password visibility const [type, setType] = useState('password'); const [onFocus, setOnFocus] = useState(false); const iconType = type === 'password' ? 'hide' : 'show'; return ( -
+
{ - // Should execute before onFocus event.preventDefault(); setType(type === 'password' ? 'text' : 'password'); }} diff --git a/packages/ui/src/components/Input/PhoneInput.test.tsx b/packages/ui/src/components/Input/PhoneInput.test.tsx new file mode 100644 index 000000000..64672afa1 --- /dev/null +++ b/packages/ui/src/components/Input/PhoneInput.test.tsx @@ -0,0 +1,80 @@ +import { render, fireEvent } from '@testing-library/react'; +import React from 'react'; + +import { defaultCountryCallingCode, countryList } from '@/hooks/use-phone-number'; + +import PhoneInput from './PhoneInput'; + +describe('Phone Input Field UI Component', () => { + const onChange = jest.fn(); + + beforeEach(() => { + onChange.mockClear(); + }); + + it('render empty PhoneInput', () => { + const { queryByText, container } = render( + + ); + expect(queryByText(`+${defaultCountryCallingCode}`)).toBeNull(); + expect(container.querySelector('input')?.value).toBe(''); + }); + + it('render with country list', () => { + const { queryByText, container } = render( + + ); + + const countryCode = queryByText(`+${defaultCountryCallingCode}`); + expect(countryCode).not.toBeNull(); + + const selector = container.querySelector('select'); + expect(selector).not.toBeNull(); + + if (selector) { + fireEvent.change(selector, { target: { value: '1' } }); + expect(onChange).toBeCalledWith({ countryCallingCode: '1' }); + } + }); + + it('render input update', () => { + const { container } = render( + + ); + + const inputField = container.querySelector('input'); + expect(inputField?.value).toBe('911'); + + if (inputField) { + fireEvent.change(inputField, { target: { value: '110' } }); + expect(onChange).toBeCalledWith({ nationalNumber: '110' }); + fireEvent.focus(inputField); + } + }); + + it('render input clear', () => { + const { container } = render( + + ); + + const inputField = container.querySelector('input'); + + if (inputField) { + fireEvent.focus(inputField); + } + + const clearButton = container.querySelector('svg'); + expect(clearButton).not.toBeNull(); + + if (clearButton) { + fireEvent.mouseDown(clearButton); + expect(onChange).toBeCalledWith({ nationalNumber: '' }); + } + }); +}); diff --git a/packages/ui/src/components/Input/PhoneInput.tsx b/packages/ui/src/components/Input/PhoneInput.tsx new file mode 100644 index 000000000..0430fcfa3 --- /dev/null +++ b/packages/ui/src/components/Input/PhoneInput.tsx @@ -0,0 +1,111 @@ +import classNames from 'classnames'; +import React, { useState, useMemo, useRef } from 'react'; + +import { CountryCallingCode, CountryMetaData } from '@/hooks/use-phone-number'; + +import { ClearIcon, DownArrowIcon } from '../Icons'; +import * as styles from './index.module.scss'; +import * as phoneInputStyles from './phoneInput.module.scss'; + +export type Props = { + name: string; + autoComplete?: AutoCompleteType; + isDisabled?: boolean; + className?: string; + placeholder?: string; + countryCallingCode?: CountryCallingCode; + nationalNumber: string; + countryList?: CountryMetaData[]; + hasError?: boolean; + onChange: (value: { countryCallingCode?: CountryCallingCode; nationalNumber?: string }) => void; +}; + +const PhoneInput = ({ + name, + autoComplete, + isDisabled, + className, + placeholder, + countryCallingCode, + nationalNumber, + countryList, + hasError = false, + onChange, +}: Props) => { + const [onFocus, setOnFocus] = useState(false); + const inputReference = useRef(null); + + const countrySelector = useMemo(() => { + if (!countryCallingCode || !countryList) { + return null; + } + + return ( +
+ {`+${countryCallingCode}`} + + +
+ ); + }, [countryCallingCode, countryList, onChange]); + + return ( +
+ {countrySelector} + { + setOnFocus(true); + }} + onBlur={() => { + setOnFocus(false); + }} + onChange={({ target: { value } }) => { + onChange({ nationalNumber: value }); + }} + /> + {nationalNumber && onFocus && ( + { + event.preventDefault(); + onChange({ nationalNumber: '' }); + }} + /> + )} +
+ ); +}; + +export default PhoneInput; diff --git a/packages/ui/src/components/Input/index.module.scss b/packages/ui/src/components/Input/index.module.scss index 889f6b7a8..367807f8d 100644 --- a/packages/ui/src/components/Input/index.module.scss +++ b/packages/ui/src/components/Input/index.module.scss @@ -2,38 +2,46 @@ .wrapper { position: relative; -} - -.input { - width: 100%; - padding: _.unit(3) _.unit(12) _.unit(3) _.unit(5); + @include _.flex-row; + padding: 0 _.unit(4); border-radius: _.unit(2); border: _.border(); background: var(--color-control-background); color: var(--color-font-primary); - caret-color: var(--color-primary); - font: var(--font-control); - transition: var(--transition-default-control); - &::placeholder { - color: var(--color-font-tertiary-3); + > *:not(:first-child) { + margin-left: _.unit(1); } - &:focus { + &.focus { border: _.border(var(--color-control-focus)); } -} -.error, -.error:focus { - border: _.border(var(--color-error)); + &.error { + border: _.border(var(--color-error)); + } + + input { + flex: 1; + border: none; + background: none; + padding: _.unit(3) 0; + caret-color: var(--color-primary); + font: var(--font-control); + transition: var(--transition-default-control); + + &::placeholder { + color: var(--color-font-tertiary-3); + } + + &:-webkit-autofill { + box-shadow: 0 0 0 30px var(--color-control-background) inset; + transition: background-color 5000s ease-in-out 0s; + } + } } .actionButton { - position: absolute; - right: _.unit(5); - bottom: 50%; - transform: translateY(50%); fill: var(--color-neutral-70); &.highlight { diff --git a/packages/ui/src/components/Input/index.test.tsx b/packages/ui/src/components/Input/index.test.tsx index ea0346ba9..79d9a632c 100644 --- a/packages/ui/src/components/Input/index.test.tsx +++ b/packages/ui/src/components/Input/index.test.tsx @@ -12,7 +12,6 @@ describe('Input Field UI Component', () => { const inputEle = container.querySelector('input'); expect(inputEle).not.toBeNull(); expect(inputEle?.value).toEqual(text); - expect(container.querySelector('svg')).not.toBeNull(); if (inputEle) { fireEvent.change(inputEle, { target: { value: 'update' } }); @@ -22,11 +21,19 @@ describe('Input Field UI Component', () => { test('click on clear button', () => { const { container } = render(); + const inputField = container.querySelector('input'); + + expect(container.querySelector('svg')).toBeNull(); + + if (inputField) { + fireEvent.focus(inputField); + } + const clearIcon = container.querySelector('svg'); expect(clearIcon).not.toBeNull(); if (clearIcon) { - fireEvent.click(clearIcon); + fireEvent.mouseDown(clearIcon); expect(onChange).toBeCalledWith(''); } }); diff --git a/packages/ui/src/components/Input/index.tsx b/packages/ui/src/components/Input/index.tsx index fc914d2c7..d7025452e 100644 --- a/packages/ui/src/components/Input/index.tsx +++ b/packages/ui/src/components/Input/index.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; +import React, { useState } from 'react'; import { ClearIcon } from '../Icons'; import * as styles from './index.module.scss'; @@ -30,15 +30,19 @@ const Input = ({ onChange, }: Props) => { const [onFocus, setOnFocus] = useState(false); - const inputReference = useRef(null); return ( -
+
{ - // Should execute before onFocus event.preventDefault(); onChange(''); }} diff --git a/packages/ui/src/components/Input/phoneInput.module.scss b/packages/ui/src/components/Input/phoneInput.module.scss new file mode 100644 index 000000000..dba0103cd --- /dev/null +++ b/packages/ui/src/components/Input/phoneInput.module.scss @@ -0,0 +1,26 @@ +@use '@/scss/underscore' as _; + +.countryCodeSelector { + color: var(--color-font-primary); + font: var(--font-control); + border: none; + background: none; + width: auto; + @include _.flex-row; + position: relative; + + > select { + appearance: none; + border: none; + outline: none; + background: none; + position: absolute; + width: 100%; + height: 100%; + font-size: 0; + } + + > svg { + fill: var(--color-primary); + } +} diff --git a/packages/ui/src/containers/PhoneInputProvider/index.test.tsx b/packages/ui/src/containers/PhoneInputProvider/index.test.tsx new file mode 100644 index 000000000..f6d77b589 --- /dev/null +++ b/packages/ui/src/containers/PhoneInputProvider/index.test.tsx @@ -0,0 +1,79 @@ +import { render, fireEvent } from '@testing-library/react'; +import React from 'react'; + +import { defaultCountryCallingCode } from '@/hooks/use-phone-number'; + +import PhoneInputProvider from '.'; + +describe('Phone Input Provider', () => { + const onChange = jest.fn(); + + beforeEach(() => { + onChange.mockClear(); + }); + + it('render with empty input', () => { + const { queryByText } = render( + + ); + + expect(queryByText(`+${defaultCountryCallingCode}`)).not.toBeNull(); + }); + + it('render with input', () => { + const { queryByText, container } = render( + + ); + + expect(queryByText('+1')).not.toBeNull(); + expect(container.querySelector('input')?.value).toEqual('911'); + }); + + it('update country code', () => { + const { container } = render( + + ); + + const selector = container.querySelector('select'); + + if (selector) { + fireEvent.change(selector, { target: { value: '86' } }); + expect(onChange).toBeCalledWith('+86911'); + } + }); + + it('update national code', () => { + const { container } = render( + + ); + + const input = container.querySelector('input'); + + if (input) { + fireEvent.change(input, { target: { value: '119' } }); + expect(onChange).toBeCalledWith('+1119'); + } + }); + + it('clear national code', () => { + const { container } = render( + + ); + + const input = container.querySelector('input'); + + if (!input) { + return; + } + + fireEvent.focus(input); + + const clearButton = container.querySelectorAll('svg'); + expect(clearButton).toHaveLength(2); + + if (clearButton[1]) { + fireEvent.mouseDown(clearButton[1]); + expect(onChange).toBeCalledWith('+1'); + } + }); +}); diff --git a/packages/ui/src/containers/PhoneInputProvider/index.tsx b/packages/ui/src/containers/PhoneInputProvider/index.tsx new file mode 100644 index 000000000..4869a43b7 --- /dev/null +++ b/packages/ui/src/containers/PhoneInputProvider/index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import PhoneInput from '@/components/Input/PhoneInput'; +import usePhoneNumber, { countryList } from '@/hooks/use-phone-number'; + +export type Props = { + name: string; + autoComplete?: AutoCompleteType; + isDisabled?: boolean; + className?: string; + placeholder?: string; + value: string; + onChange: (value: string) => void; +}; + +const PhoneInputProvider = ({ value, onChange, ...inputProps }: Props) => { + // TODO: error message + const { + error, + phoneNumber: { countryCallingCode, nationalNumber, interacted }, + setPhoneNumber, + } = usePhoneNumber(value, onChange); + + return ( + { + setPhoneNumber((phoneNumber) => ({ ...phoneNumber, ...data, interacted: true })); + }} + /> + ); +}; + +export default PhoneInputProvider; diff --git a/packages/ui/src/hooks/use-phone-number.ts b/packages/ui/src/hooks/use-phone-number.ts new file mode 100644 index 000000000..7a9a09729 --- /dev/null +++ b/packages/ui/src/hooks/use-phone-number.ts @@ -0,0 +1,128 @@ +/** + * Provide PhoneNumber Format support + * Reference [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js) + */ + +import { + parsePhoneNumber as _parsePhoneNumber, + getCountries, + getCountryCallingCode, + CountryCallingCode, + CountryCode, + E164Number, + ParseError, +} from 'libphonenumber-js'; +import { useState, useEffect } from 'react'; +// Should not need the react-phone-number-input package, but we use its locale country name for now +import en from 'react-phone-number-input/locale/en.json'; + +export type { CountryCallingCode } from 'libphonenumber-js'; + +/** + * TODO: Get Default Country Code + */ +const defaultCountryCode: CountryCode = 'CN'; + +export const defaultCountryCallingCode: CountryCallingCode = + getCountryCallingCode(defaultCountryCode); + +/** + * Provide Country Code Options + * TODO: Country Name i18n + */ +export type CountryMetaData = { + countryCode: CountryCode; + countryCallingCode: CountryCallingCode; + countryName?: string; +}; + +export const countryList: CountryMetaData[] = getCountries().map((code) => { + const callingCode = getCountryCallingCode(code); + const countryName = en[code]; + + return { + countryCode: code, + countryCallingCode: callingCode, + countryName, + }; +}); + +type PhoneNumberData = { + countryCallingCode: string; + nationalNumber: string; +}; + +// Add interact status to prevent the initial onUpdate useEffect call +type PhoneNumberState = PhoneNumberData & { interacted: boolean }; + +const parseE164Number = (value: string): E164Number | '' => { + if (!value || value.startsWith('+')) { + return value; + } + + return `+${value}`; +}; + +export const parsePhoneNumber = (value: string): [ParseError?, PhoneNumberData?] => { + try { + const phoneNumber = _parsePhoneNumber(parseE164Number(value)); + const { countryCallingCode, nationalNumber } = phoneNumber; + + return [undefined, { countryCallingCode, nationalNumber }]; + } catch (error: unknown) { + if (error instanceof ParseError) { + return [error]; + } + throw error; + } +}; + +const usePhoneNumber = (value: string, onChangeCallback: (value: string) => void) => { + // TODO: phoneNumber format based on country + + const [phoneNumber, setPhoneNumber] = useState({ + countryCallingCode: defaultCountryCallingCode, + nationalNumber: '', + interacted: false, + }); + const [error, setError] = useState(); + + useEffect(() => { + // Only run on data initialization + if (phoneNumber.interacted) { + return; + } + + const [parseError, result] = parsePhoneNumber(value); + setError(parseError); + + if (result) { + const { countryCallingCode, nationalNumber } = result; + setPhoneNumber((previous) => ({ + ...previous, + countryCallingCode, + nationalNumber, + })); + } + }, [phoneNumber.interacted, value]); + + useEffect(() => { + // Only run after data initialization + if (!phoneNumber.interacted) { + return; + } + + const { countryCallingCode, nationalNumber } = phoneNumber; + const [parseError] = parsePhoneNumber(`${countryCallingCode}${nationalNumber}`); + setError(parseError); + onChangeCallback(`+${countryCallingCode}${nationalNumber}`); + }, [onChangeCallback, phoneNumber]); + + return { + error, + phoneNumber, + setPhoneNumber, + }; +}; + +export default usePhoneNumber; diff --git a/packages/ui/src/include.d/dom.d.ts b/packages/ui/src/include.d/dom.d.ts index d6c0c1615..ce3570903 100644 --- a/packages/ui/src/include.d/dom.d.ts +++ b/packages/ui/src/include.d/dom.d.ts @@ -68,7 +68,8 @@ type AutoCompleteType = | 'bday-year' | 'sex' | 'url' - | 'photo'; + | 'photo' + | 'mobile'; // TO-DO: remove me interface Body { diff --git a/packages/ui/src/scss/_underscore.scss b/packages/ui/src/scss/_underscore.scss index fe29ed9d6..d84efcb45 100644 --- a/packages/ui/src/scss/_underscore.scss +++ b/packages/ui/src/scss/_underscore.scss @@ -9,6 +9,12 @@ justify-content: center; } +@mixin flex-row { + display: flex; + align-items: center; + justify-content: center; +} + @mixin image-align-center { object-fit: contain; object-position: center; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6df382b6..c77347418 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -288,7 +288,9 @@ importers: i18next-browser-languagedetector: ^6.1.3 identity-obj-proxy: ^3.0.0 jest: ^27.5.1 + jest-transform-stub: ^2.0.0 ky: ^0.29.0 + libphonenumber-js: ^1.9.49 lint-staged: ^11.1.1 parcel: ^2.3.2 postcss: ^8.4.6 @@ -297,6 +299,7 @@ importers: react: ^17.0.2 react-dom: ^17.0.2 react-i18next: ^11.15.4 + react-phone-number-input: ^3.1.46 react-router-dom: ^5.2.0 stylelint: ^13.13.1 ts-jest: ^27.0.5 @@ -308,9 +311,11 @@ importers: i18next: 21.6.11 i18next-browser-languagedetector: 6.1.3 ky: 0.29.0 + libphonenumber-js: 1.9.49 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 react-i18next: 11.15.4_3fb644aa30122a07f960d67fa51d6dc1 + react-phone-number-input: 3.1.46_react-dom@17.0.2+react@17.0.2 react-router-dom: 5.3.0_react@17.0.2 devDependencies: '@jest/types': 27.5.1 @@ -328,6 +333,7 @@ importers: eslint: 8.10.0 identity-obj-proxy: 3.0.0 jest: 27.5.1 + jest-transform-stub: 2.0.0 lint-staged: 11.2.6 parcel: 2.3.2_postcss@8.4.6 postcss: 8.4.6 @@ -2216,27 +2222,6 @@ packages: write-file-atomic: 3.0.3 dev: true - /@monaco-editor/loader/1.2.0_monaco-editor@0.32.1: - resolution: {integrity: sha512-cJVCG/T/KxXgzYnjKqyAgsKDbH9mGLjcXxN6AmwumBwa2rVFkwvGcUj1RJtD0ko4XqLqJxwqsN/Z/KURB5f1OQ==} - peerDependencies: - monaco-editor: '>= 0.21.0 < 1' - dependencies: - monaco-editor: 0.32.1 - state-local: 1.0.7 - dev: false - - /@monaco-editor/react/4.3.1_e62f1489d5efe674a41c3f8d6971effe: - resolution: {integrity: sha512-f+0BK1PP/W5I50hHHmwf11+Ea92E5H1VZXs+wvKplWUWOfyMa1VVwqkJrXjRvbcqHL+XdIGYWhWNdi4McEvnZg==} - peerDependencies: - monaco-editor: '>= 0.25.0 < 1' - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - dependencies: - '@monaco-editor/loader': 1.2.0_monaco-editor@0.32.1 - monaco-editor: 0.32.1 - prop-types: 15.8.1 - react: 17.0.2 - react-dom: 17.0.2_react@17.0.2 /@logto/browser/0.1.2: resolution: {integrity: sha512-sTJjnx00BXYEChCbbO/LPs0x0wE1bDSHniFi+u93cynyEHgoT5yjMnH4N39NhrpmRdkXxOxaIkXmyAT1nSmYzQ==} requiresBuild: true @@ -2271,6 +2256,29 @@ packages: react: 17.0.2 dev: false + /@monaco-editor/loader/1.2.0_monaco-editor@0.32.1: + resolution: {integrity: sha512-cJVCG/T/KxXgzYnjKqyAgsKDbH9mGLjcXxN6AmwumBwa2rVFkwvGcUj1RJtD0ko4XqLqJxwqsN/Z/KURB5f1OQ==} + peerDependencies: + monaco-editor: '>= 0.21.0 < 1' + dependencies: + monaco-editor: 0.32.1 + state-local: 1.0.7 + dev: false + + /@monaco-editor/react/4.3.1_e62f1489d5efe674a41c3f8d6971effe: + resolution: {integrity: sha512-f+0BK1PP/W5I50hHHmwf11+Ea92E5H1VZXs+wvKplWUWOfyMa1VVwqkJrXjRvbcqHL+XdIGYWhWNdi4McEvnZg==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + dependencies: + '@monaco-editor/loader': 1.2.0_monaco-editor@0.32.1 + monaco-editor: 0.32.1 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: false + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -5033,6 +5041,10 @@ packages: yaml: 1.10.2 dev: true + /country-flag-icons/1.4.21: + resolution: {integrity: sha512-bA9jDr+T5li7EsKdDx0xVnO0bdMdoT8IA3BNbeT2XSWUygR1okhiZ2+eYiC1EKLrFZhI4aEHni2w03lUlOjogg==} + dev: false + /crack-json/1.3.0: resolution: {integrity: sha512-JfZ9NPLsU9ejTYgZ7fM+5TIMfTwROTxpi2Twh597GxmiVDwIGZSjaor+zsQBKZ0mmCKOFb9EZZLVeKNf/5UaGg==} engines: {node: '>=8.0'} @@ -7243,6 +7255,12 @@ packages: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: false + /input-format/0.3.7: + resolution: {integrity: sha512-hgwiCjV7MnhFvX4Hwrvk7hB2a2rcB2CQb7Ex7GlK1ISbEXuLtflwBUnadFSA1rVNDPFh9yWBaJJ4/o1XkzhPIg==} + dependencies: + prop-types: 15.8.1 + dev: false + /inquirer/7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} @@ -8477,6 +8495,10 @@ packages: - supports-color dev: true + /jest-transform-stub/2.0.0: + resolution: {integrity: sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==} + dev: true + /jest-util/27.4.2: resolution: {integrity: sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -9016,6 +9038,10 @@ packages: - supports-color dev: true + /libphonenumber-js/1.9.49: + resolution: {integrity: sha512-/wEOIONcVboFky+lWlCaF7glm1FhBz11M5PHeCApA+xDdVfmhKjHktHS8KjyGxouV5CSXIr4f3GvLSpJa4qMSg==} + dev: false + /lilconfig/2.0.4: resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==} engines: {node: '>=10'} @@ -11923,6 +11949,21 @@ packages: warning: 4.0.3 dev: false + /react-phone-number-input/3.1.46_react-dom@17.0.2+react@17.0.2: + resolution: {integrity: sha512-afYl7BMy/0vMqWtzsZBmOgiPdqQAGyPO/Z3auorFs4K/zgFSBq3YoaASleodBkeRO/PygJ4ML8Wnb4Ce+3dlVQ==} + peerDependencies: + react: '>=0.16.8' + react-dom: '>=0.16.8' + dependencies: + classnames: 2.3.1 + country-flag-icons: 1.4.21 + input-format: 0.3.7 + libphonenumber-js: 1.9.49 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: false + /react-refresh/0.9.0: resolution: {integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==} engines: {node: '>=0.10.0'}